mirror of
https://github.com/documenso/documenso.git
synced 2025-11-16 09:41:35 +10:00
wip: refresh design
This commit is contained in:
@ -1,36 +0,0 @@
|
||||
import { Button } from "@documenso/ui";
|
||||
import Logo from "../components/logo";
|
||||
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Custom404() {
|
||||
return (
|
||||
<>
|
||||
<main className="relative isolate min-h-full bg-gray-100">
|
||||
<Link href="/" className="absolute top-10 left-10 flex gap-x-2 items-center">
|
||||
<Logo className="w-10 text-black" />
|
||||
<h2 className="text-2xl font-semibold">Documenso</h2>
|
||||
</Link>
|
||||
|
||||
<div className="mx-auto max-w-7xl px-6 py-48 text-center sm:py-40 lg:px-8">
|
||||
<p className="text-brown text-base font-semibold leading-8">404</p>
|
||||
<h1 className="text-brown mt-4 text-3xl font-bold tracking-tight sm:text-5xl">
|
||||
Page not found
|
||||
</h1>
|
||||
<p className="mt-4 text-base text-gray-700 sm:mt-6">
|
||||
Sorry, we couldn’t find the page you’re looking for.
|
||||
</p>
|
||||
<div className="mt-10 flex justify-center">
|
||||
<Button
|
||||
color="secondary"
|
||||
href="/"
|
||||
icon={ArrowSmallLeftIcon}
|
||||
className="text-brown text-base font-semibold leading-7">
|
||||
Back to home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { Button } from "@documenso/ui";
|
||||
import Logo from "../components/logo";
|
||||
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
|
||||
import { EllipsisVerticalIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export default function Custom500() {
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex min-h-full flex-col items-center justify-center bg-black text-white">
|
||||
<Link href="/" className="absolute top-10 left-10 flex items-center gap-x-2 invert">
|
||||
<Logo className="w-10 text-black" />
|
||||
<h2 className="text-2xl font-semibold text-black">Documenso</h2>
|
||||
</Link>
|
||||
|
||||
<div className="mt-20 max-w-7xl px-4 py-10">
|
||||
<p className="inline-flex items-center text-3xl font-bold sm:text-5xl">
|
||||
500
|
||||
<span className="relative -top-1.5 px-3 font-thin sm:text-6xl">|</span>{" "}
|
||||
<span className="align-middle text-base font-semibold sm:text-2xl">
|
||||
Something went wrong.
|
||||
</span>
|
||||
</p>
|
||||
<div className="mt-10 flex justify-center">
|
||||
<Button color="secondary" href="/" icon={ArrowSmallLeftIcon}>
|
||||
Back to home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
# Docker config for render.com
|
||||
# Be sure to add an .env config to your docker web service
|
||||
FROM node:19.5.0-alpine
|
||||
RUN apk add --no-cache openjdk11
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Inserted from render.com ENV Group
|
||||
ARG DATABASE_URL
|
||||
ARG MAIL_FROM
|
||||
ARG NEXT_PUBLIC_WEBAPP_URL
|
||||
ARG NEXTAUTH_SECRET
|
||||
ARG NEXTAUTH_URL
|
||||
ARG SENDGRID_API_KEY
|
||||
|
||||
# Fill docker ENV variables with render.com ENV Group - BUILD-TIME
|
||||
ENV DATABASE_URL=$DATABASE_URL \
|
||||
MAIL_FROM=$ \
|
||||
NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL \
|
||||
NEXTAUTH_SECRET=&NEXTAUTH_SECRET \
|
||||
NEXTAUTH_URL=$NEXTAUTH_URL \
|
||||
SENDGRID_API_KEY=$SENDGRID_API_KEY
|
||||
|
||||
COPY . /app
|
||||
RUN npm run build
|
||||
|
||||
# No runtime ENV Variables set so far besides ENV
|
||||
ENV NODE_ENV production
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
@ -1,52 +0,0 @@
|
||||
import { ReactElement, ReactNode } from "react";
|
||||
import { NextPage } from "next";
|
||||
import type { AppProps } from "next/app";
|
||||
import { Montserrat, Qwigley } from "next/font/google";
|
||||
import { SubscriptionProvider } from "@documenso/lib/stripe/providers/subscription-provider";
|
||||
import "../../../node_modules/placeholder-loading/src/scss/placeholder-loading.scss";
|
||||
import "../../../node_modules/react-resizable/css/styles.css";
|
||||
import "../styles/tailwind.css";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import "react-tooltip/dist/react-tooltip.css";
|
||||
|
||||
export { coloredConsole } from "@documenso/lib";
|
||||
|
||||
const montserrat = Montserrat({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "700"],
|
||||
display: "swap",
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
const qwigley = Qwigley({
|
||||
subsets: ["latin"],
|
||||
weight: ["400"],
|
||||
display: "swap",
|
||||
variable: "--font-qwigley",
|
||||
});
|
||||
|
||||
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||
getLayout?: (page: ReactElement) => ReactNode;
|
||||
};
|
||||
|
||||
type AppPropsWithLayout = AppProps & {
|
||||
Component: NextPageWithLayout;
|
||||
};
|
||||
|
||||
export default function App({
|
||||
Component,
|
||||
pageProps: { session, initialSubscription, ...pageProps },
|
||||
}: AppPropsWithLayout) {
|
||||
const getLayout = Component.getLayout || ((page: any) => page);
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<SubscriptionProvider initialSubscription={initialSubscription}>
|
||||
<main className={`${montserrat.variable} h-full font-sans`}>
|
||||
<Toaster position="top-center" />
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</main>
|
||||
</SubscriptionProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
import { Head, Html, Main, NextScript } from "next/document";
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html className="h-full scroll-smooth bg-gray-100 font-normal antialiased" lang="en">
|
||||
<Head>
|
||||
<meta name="color-scheme"></meta>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
</Head>
|
||||
<body className="flex h-full flex-col">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
import { ErrorCode } from "@documenso/lib/auth";
|
||||
import { verifyPassword } from "@documenso/lib/auth";
|
||||
import prisma from "@documenso/prisma";
|
||||
import NextAuth, { Session } from "next-auth";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import GitHubProvider from "next-auth/providers/github";
|
||||
|
||||
export default NextAuth({
|
||||
secret: process.env.AUTH_SECRET,
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
signOut: "/login",
|
||||
error: "/auth/error", // Error code passed in query string as ?error=
|
||||
verifyRequest: "/auth/verify-request", // (used for check email message)
|
||||
},
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
id: "credentials",
|
||||
name: "Documenso.com Login",
|
||||
type: "credentials",
|
||||
credentials: {
|
||||
email: {
|
||||
label: "Email Address",
|
||||
type: "email",
|
||||
placeholder: "john.doe@example.com",
|
||||
},
|
||||
password: {
|
||||
label: "Password",
|
||||
type: "password",
|
||||
placeholder: "Select a password. Here is some inspiration: https://xkcd.com/936/",
|
||||
},
|
||||
},
|
||||
async authorize(credentials: any) {
|
||||
if (!credentials) {
|
||||
console.error("Credential missing in authorize()");
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: credentials.email.toLowerCase(),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
password: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error(ErrorCode.UserNotFound);
|
||||
}
|
||||
|
||||
if (!user.password) {
|
||||
throw new Error(ErrorCode.UserMissingPassword);
|
||||
}
|
||||
|
||||
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
|
||||
|
||||
if (!isCorrectPassword) {
|
||||
throw new Error(ErrorCode.IncorrectPassword);
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user, account }) {
|
||||
return {
|
||||
...token,
|
||||
};
|
||||
},
|
||||
async session({ session, token }) {
|
||||
const documensoSession: Session = {
|
||||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
},
|
||||
};
|
||||
|
||||
documensoSession.expires;
|
||||
return documensoSession;
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -1,56 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { hashPassword } from "@documenso/lib/auth";
|
||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { email, password, source } = req.body;
|
||||
const cleanEmail = email.toLowerCase();
|
||||
|
||||
if (!cleanEmail || !/.+@.+/.test(cleanEmail)) {
|
||||
res.status(400).json({ message: "Invalid email" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password || password.trim().length < 7) {
|
||||
return res.status(400).json({
|
||||
message: "Password should be at least 7 characters long.",
|
||||
});
|
||||
}
|
||||
|
||||
// User already exists if email already exists
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: cleanEmail,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
const message: string = "This email is already registered.";
|
||||
return res.status(409).json({ message });
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
await prisma.user.upsert({
|
||||
where: { email: cleanEmail },
|
||||
update: {
|
||||
password: hashedPassword,
|
||||
emailVerified: new Date(Date.now()),
|
||||
identityProvider: IdentityProvider.DOCUMENSO,
|
||||
},
|
||||
create: {
|
||||
email: cleanEmail,
|
||||
password: hashedPassword,
|
||||
identityProvider: IdentityProvider.DOCUMENSO,
|
||||
source: source,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).end();
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||
});
|
||||
@ -1,95 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { id: documentId } = req.query;
|
||||
const { token: recipientToken } = req.query;
|
||||
|
||||
if (!documentId) {
|
||||
return res.status(400).send("Missing parameter documentId.");
|
||||
}
|
||||
|
||||
let user = null;
|
||||
let recipient = null;
|
||||
if (recipientToken) {
|
||||
// Request from signing page without login
|
||||
recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
token: recipientToken?.toString(),
|
||||
},
|
||||
include: {
|
||||
Document: { include: { User: true } },
|
||||
},
|
||||
});
|
||||
user = recipient?.Document.User;
|
||||
} else {
|
||||
// Request from editor with valid user login
|
||||
user = await getUserFromToken(req, res);
|
||||
}
|
||||
|
||||
if (!user) return res.status(401).end();
|
||||
|
||||
let document: PrismaDocument | null = null;
|
||||
if (recipientToken) {
|
||||
document = await prisma.document.findFirst({
|
||||
where: { id: recipient?.Document?.id },
|
||||
});
|
||||
} else {
|
||||
document = await getDocument(+documentId, req, res);
|
||||
}
|
||||
|
||||
if (!document) res.status(404).end(`No document with id ${documentId} found.`);
|
||||
|
||||
const signaturesCount = await prisma.signature.count({
|
||||
where: {
|
||||
Field: {
|
||||
documentId: document?.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let signedDocumentAsBase64 = document?.document || "";
|
||||
|
||||
// No need to add a signature, if no one signed yet.
|
||||
if (signaturesCount > 0) {
|
||||
signedDocumentAsBase64 = await addDigitalSignature(document?.document || "");
|
||||
}
|
||||
|
||||
const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64");
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader("Content-Length", buffer.length);
|
||||
res.setHeader("Content-Disposition", `attachment; filename=${document?.title}`);
|
||||
|
||||
return res.status(200).send(buffer);
|
||||
}
|
||||
|
||||
async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
const { id: documentId } = req.query;
|
||||
|
||||
if (!user) return;
|
||||
|
||||
if (!documentId) {
|
||||
res.status(400).send("Missing parameter documentId.");
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.document
|
||||
.delete({
|
||||
where: {
|
||||
id: +documentId,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
res.status(200).end();
|
||||
});
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
DELETE: Promise.resolve({ default: defaultResponder(deleteHandler) }),
|
||||
});
|
||||
@ -1,32 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { FieldType, Document as PrismaDocument } from "@prisma/client";
|
||||
import short from "short-uuid";
|
||||
|
||||
async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
const { fid: fieldId } = req.query;
|
||||
const body: {
|
||||
id: number;
|
||||
type: FieldType;
|
||||
page: number;
|
||||
position: { x: number; y: number };
|
||||
} = req.body;
|
||||
|
||||
if (!user) return;
|
||||
|
||||
if (!fieldId) {
|
||||
res.status(400).send("Missing parameter fieldId.");
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.field.delete({ where: { id: +fieldId } });
|
||||
|
||||
return res.status(200).end();
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
DELETE: Promise.resolve({ default: defaultResponder(deleteHandler) }),
|
||||
});
|
||||
@ -1,101 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { FieldType, Document as PrismaDocument } from "@prisma/client";
|
||||
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
const { id: documentId } = req.query;
|
||||
const body: {
|
||||
id: number;
|
||||
type: FieldType;
|
||||
page: number;
|
||||
position: { x: number; y: number };
|
||||
} = req.body;
|
||||
|
||||
if (!user) return;
|
||||
|
||||
if (!documentId) {
|
||||
res.status(400).send("Missing parameter documentId.");
|
||||
return;
|
||||
}
|
||||
|
||||
// todo entity ownerships checks
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { documentId: +documentId },
|
||||
include: { Recipient: true },
|
||||
});
|
||||
|
||||
return res.status(200).end(JSON.stringify(fields));
|
||||
}
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { token: recipientToken } = req.query;
|
||||
let user = null;
|
||||
if (!recipientToken) user = await getUserFromToken(req, res);
|
||||
if (!user && !recipientToken) return res.status(401).end();
|
||||
const body: {
|
||||
id: number;
|
||||
type: FieldType;
|
||||
page: number;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
Recipient: { id: number };
|
||||
customText: string;
|
||||
} = req.body;
|
||||
|
||||
const { id: documentId } = req.query;
|
||||
if (!documentId) {
|
||||
return res.status(400).send("Missing parameter documentId.");
|
||||
}
|
||||
|
||||
if (recipientToken) {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: { token: recipientToken?.toString() },
|
||||
});
|
||||
|
||||
if (!recipient || recipient?.documentId !== +documentId)
|
||||
return res.status(401).send("Recipient does not have access to this document.");
|
||||
}
|
||||
|
||||
if (user) {
|
||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||
// todo entity ownerships checks
|
||||
if (document.userId !== user.id) {
|
||||
return res.status(401).send("User does not have access to this document.");
|
||||
}
|
||||
}
|
||||
|
||||
const field = await prisma.field.upsert({
|
||||
where: {
|
||||
id: +body.id,
|
||||
},
|
||||
update: {
|
||||
positionX: +body.positionX,
|
||||
positionY: +body.positionY,
|
||||
customText: body.customText,
|
||||
},
|
||||
create: {
|
||||
documentId: +documentId,
|
||||
type: body.type,
|
||||
page: +body.page,
|
||||
inserted: false,
|
||||
positionX: +body.positionX,
|
||||
positionY: +body.positionY,
|
||||
customText: body.customText,
|
||||
recipientId: body.Recipient.id,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(201).end(JSON.stringify(field));
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||
});
|
||||
@ -1,29 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
import short from "short-uuid";
|
||||
|
||||
async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
const { id: documentId, rid: recipientId } = req.query;
|
||||
const body = req.body;
|
||||
|
||||
if (!recipientId) {
|
||||
res.status(400).send("Missing parameter recipientId.");
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.recipient.delete({
|
||||
where: {
|
||||
id: +recipientId,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).end();
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
DELETE: Promise.resolve({ default: defaultResponder(deleteHandler) }),
|
||||
});
|
||||
@ -1,48 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
import short from "short-uuid";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
const { id: documentId } = req.query;
|
||||
const body: { name: string; email: string; id: string } = req.body;
|
||||
|
||||
if (!user) return;
|
||||
|
||||
if (!documentId) {
|
||||
res.status(400).send("Missing parameter documentId.");
|
||||
return;
|
||||
}
|
||||
|
||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||
|
||||
// todo entity ownerships checks
|
||||
if (document.userId !== user.id) {
|
||||
return res.status(401).send("User does not have access to this document.");
|
||||
}
|
||||
|
||||
const recipient = await prisma.recipient.upsert({
|
||||
where: {
|
||||
id: +body.id,
|
||||
},
|
||||
update: {
|
||||
email: body.email.toString(),
|
||||
name: body.name.toString(),
|
||||
},
|
||||
create: {
|
||||
documentId: +documentId,
|
||||
email: body.email.toString(),
|
||||
name: body.name.toString(),
|
||||
token: short.generate().toString(),
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).end(JSON.stringify(recipient));
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||
});
|
||||
@ -1,72 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { sendSigningRequest } from "@documenso/lib/mail";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { Document as PrismaDocument, SendStatus } from "@prisma/client";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const user = await getUserFromToken(req, res);
|
||||
const { id: documentId } = req.query;
|
||||
const { resendTo: resendTo = [] } = req.body;
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).send("Unauthorized");
|
||||
}
|
||||
|
||||
if (!documentId) {
|
||||
return res.status(400).send("Missing parameter documentId.");
|
||||
}
|
||||
|
||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||
|
||||
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 = {
|
||||
documentId: +documentId,
|
||||
id: { in: resendTo },
|
||||
};
|
||||
}
|
||||
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
...recipientCondition,
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipients.length) {
|
||||
return res.status(200).send(recipients.length);
|
||||
}
|
||||
|
||||
let sentRequests = 0;
|
||||
|
||||
await Promise.all(
|
||||
recipients.map(async (recipient) => {
|
||||
await sendSigningRequest(recipient, document, user);
|
||||
|
||||
sentRequests++;
|
||||
})
|
||||
);
|
||||
|
||||
if (sentRequests === recipients.length) {
|
||||
return res.status(200).send(recipients.length);
|
||||
}
|
||||
|
||||
return res.status(502).end("Could not send request for signing.");
|
||||
} catch (err) {
|
||||
return res.status(502).end("Could not send request for signing.");
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||
});
|
||||
@ -1,191 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { sendSigningDoneMail } from "@documenso/lib/mail";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { DocumentStatus, SigningStatus } from "@prisma/client";
|
||||
import { FieldType, Document as PrismaDocument } from "@prisma/client";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { token: recipientToken } = req.query;
|
||||
const { signatures: signaturesFromBody }: { signatures: any[] } = req.body;
|
||||
|
||||
if (!recipientToken) {
|
||||
return res.status(401).send("Missing recipient token.");
|
||||
}
|
||||
|
||||
// The recipient who received the signing request
|
||||
const recipient = await prisma.recipient.findFirstOrThrow({
|
||||
where: { token: recipientToken?.toString() },
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
return res.status(401).send("Recipient not found.");
|
||||
}
|
||||
|
||||
const document: PrismaDocument = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: recipient.documentId,
|
||||
},
|
||||
include: {
|
||||
Recipient: {
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
},
|
||||
Field: { include: { Recipient: true, Signature: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) res.status(404).end(`No document found.`);
|
||||
|
||||
let documentWithInserts = document.document;
|
||||
for (const signature of signaturesFromBody) {
|
||||
if (!signature.signatureImage && !signature.typedSignature) {
|
||||
documentWithInserts = document.document;
|
||||
throw new Error("Can't save invalid signature.");
|
||||
}
|
||||
|
||||
await saveSignature(signature);
|
||||
|
||||
const signedField = await prisma.field.findFirstOrThrow({
|
||||
where: { id: signature.fieldId },
|
||||
include: { Signature: true },
|
||||
});
|
||||
|
||||
await insertSignatureInDocument(signedField);
|
||||
}
|
||||
|
||||
await prisma.recipient.update({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
data: {
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
signedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
const unsignedRecipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId: recipient.documentId,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
});
|
||||
|
||||
const signedRecipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId: recipient.documentId,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
},
|
||||
});
|
||||
|
||||
// Don't check for inserted, because currently no "sign again" scenarios exist and
|
||||
// this is probably the expected behaviour in unclean states.
|
||||
const nonSignatureFields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
type: { in: [FieldType.DATE, FieldType.TEXT] },
|
||||
recipientId: { in: signedRecipients.map((r) => r.id) },
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
}
|
||||
});
|
||||
|
||||
// Insert fields other than signatures
|
||||
for (const field of nonSignatureFields) {
|
||||
documentWithInserts = await insertTextInPDF(
|
||||
documentWithInserts,
|
||||
field.type === FieldType.DATE
|
||||
? new Intl.DateTimeFormat("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
}).format(field.Recipient?.signedAt ?? new Date())
|
||||
: field.customText || "",
|
||||
field.positionX,
|
||||
field.positionY,
|
||||
field.page,
|
||||
false
|
||||
);
|
||||
|
||||
await prisma.field.update({
|
||||
where: {
|
||||
id: field.id,
|
||||
},
|
||||
data: {
|
||||
inserted: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.document.update({
|
||||
where: {
|
||||
id: recipient.documentId,
|
||||
},
|
||||
data: {
|
||||
document: documentWithInserts,
|
||||
status: unsignedRecipients.length > 0 ? DocumentStatus.PENDING : DocumentStatus.COMPLETED,
|
||||
},
|
||||
});
|
||||
|
||||
if (unsignedRecipients.length === 0) {
|
||||
const documentOwner = await prisma.user.findFirstOrThrow({
|
||||
where: { id: document.userId },
|
||||
select: { email: true, name: true },
|
||||
});
|
||||
|
||||
document.document = documentWithInserts;
|
||||
if (documentOwner) await sendSigningDoneMail(document, documentOwner);
|
||||
|
||||
for (const signer of signedRecipients) {
|
||||
await sendSigningDoneMail(document, signer);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).end();
|
||||
|
||||
async function insertSignatureInDocument(signedField: any) {
|
||||
if (signedField?.Signature?.signatureImageAsBase64) {
|
||||
documentWithInserts = await insertImageInPDF(
|
||||
documentWithInserts,
|
||||
signedField.Signature ? signedField.Signature?.signatureImageAsBase64 : "",
|
||||
signedField.positionX,
|
||||
signedField.positionY,
|
||||
signedField.page
|
||||
);
|
||||
} else if (signedField?.Signature?.typedSignature) {
|
||||
documentWithInserts = await insertTextInPDF(
|
||||
documentWithInserts,
|
||||
signedField.Signature.typedSignature,
|
||||
signedField.positionX,
|
||||
signedField.positionY,
|
||||
signedField.page
|
||||
);
|
||||
} else {
|
||||
documentWithInserts = document.document;
|
||||
throw new Error("Invalid signature could not be inserted.");
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSignature(signature: any) {
|
||||
await prisma.signature.upsert({
|
||||
where: {
|
||||
fieldId: signature.fieldId,
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
recipientId: recipient.id,
|
||||
fieldId: signature.fieldId,
|
||||
signatureImageAsBase64: signature.signatureImage ? signature.signatureImage : null,
|
||||
typedSignature: signature.typedSignature ? signature.typedSignature : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||
});
|
||||
@ -1,70 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
|
||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import formidable from "formidable";
|
||||
import { isSubscribedServer } from "@documenso/lib/stripe";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const form = formidable();
|
||||
|
||||
const user = await getUserFromToken(req, res);
|
||||
if (!user) {
|
||||
return res.status(401).end();
|
||||
};
|
||||
|
||||
const isSubscribed = await isSubscribedServer(req);
|
||||
|
||||
if (!isSubscribed) {
|
||||
throw new Error("User is not subscribed.");
|
||||
}
|
||||
|
||||
|
||||
form.parse(req, async (err, fields, files) => {
|
||||
if (err) throw err;
|
||||
|
||||
const uploadedDocument: any = files["document"];
|
||||
const title = uploadedDocument[0].originalFilename;
|
||||
const path = uploadedDocument[0].filepath;
|
||||
const fs = require("fs");
|
||||
const buffer = fs.readFileSync(path);
|
||||
const documentAsBase64EncodedString = buffer.toString("base64");
|
||||
await prisma
|
||||
.$transaction([
|
||||
prisma.document.create({
|
||||
data: {
|
||||
title: title,
|
||||
userId: user?.id,
|
||||
document: documentAsBase64EncodedString,
|
||||
},
|
||||
}),
|
||||
])
|
||||
.then((document) => {
|
||||
return res.status(201).send(document[0].id);
|
||||
})
|
||||
.catch((err) => {
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
if (!user) return;
|
||||
|
||||
const documents = await getDocumentsForUserFromToken({ req: req, res: res });
|
||||
|
||||
return res.status(200).json(documents);
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||
});
|
||||
@ -1,15 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
|
||||
// Return a healthy 200 status code for uptime monitoring and render.com zero-downtime-deploy
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Some generic database access to make sure the service is healthy.
|
||||
const users = await prisma.user.findFirst();
|
||||
|
||||
return res.status(200).json({ message: "Api up and running :)" });
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
@ -1 +0,0 @@
|
||||
export { checkoutSessionHandler as default } from '@documenso/lib/stripe/handlers/checkout-session'
|
||||
@ -1 +0,0 @@
|
||||
export { portalSessionHandler as default } from "@documenso/lib/stripe/handlers/portal-session";
|
||||
@ -1 +0,0 @@
|
||||
export { getSubscriptionHandler as default } from '@documenso/lib/stripe/handlers/get-subscription'
|
||||
@ -1,5 +0,0 @@
|
||||
export const config = {
|
||||
api: { bodyParser: false },
|
||||
};
|
||||
|
||||
export { webhookHandler as default } from "@documenso/lib/stripe/handlers/webhook";
|
||||
@ -1,23 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
|
||||
// todo remove before launch
|
||||
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const documentId = req.query.id || 1;
|
||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||
const signedDocument = await addDigitalSignature(document.document);
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader("Content-Length", signedDocument.length);
|
||||
res.setHeader("Content-Disposition", `attachment; filename=${document.title}`);
|
||||
|
||||
return res.status(200).send(signedDocument);
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
@ -1,53 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { hashPassword } from "@documenso/lib/auth";
|
||||
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { method, body } = req;
|
||||
|
||||
if (!body.email) {
|
||||
return res.status(400).json({ message: "Email cannot be empty." });
|
||||
}
|
||||
|
||||
let newUser: any;
|
||||
newUser = await prisma.user
|
||||
.create({
|
||||
data: { email: body.email },
|
||||
})
|
||||
.then(async () => {
|
||||
return res.status(201).send(newUser);
|
||||
});
|
||||
}
|
||||
|
||||
async function patchHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
if (!user) return;
|
||||
|
||||
const updatedUser = req.body;
|
||||
|
||||
let password: string | undefined = undefined;
|
||||
|
||||
if (typeof updatedUser.password === "string" && updatedUser.password.length >= 6) {
|
||||
password = await hashPassword(updatedUser.password);
|
||||
}
|
||||
|
||||
await prisma.user
|
||||
.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
name: updatedUser.name,
|
||||
password,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
return res.status(200).end();
|
||||
});
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||
PATCH: Promise.resolve({ default: defaultResponder(patchHandler) }),
|
||||
});
|
||||
@ -1,23 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
|
||||
if (!user) return;
|
||||
|
||||
return prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
@ -1,177 +0,0 @@
|
||||
import { ChangeEvent, ReactElement } from "react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { uploadDocument } from "@documenso/features";
|
||||
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import Layout from "../components/layout";
|
||||
import type { NextPageWithLayout } from "./_app";
|
||||
import {
|
||||
CheckBadgeIcon,
|
||||
DocumentIcon,
|
||||
ExclamationTriangleIcon,
|
||||
UsersIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import {
|
||||
DocumentStatus,
|
||||
Document as PrismaDocument,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
} from "@prisma/client";
|
||||
import { truncate } from "fs";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
import { useSubscription } from "@documenso/lib/stripe";
|
||||
|
||||
type FormValues = {
|
||||
document: File;
|
||||
};
|
||||
|
||||
const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
const { hasSubscription } = useSubscription();
|
||||
|
||||
const stats = [
|
||||
{
|
||||
name: "Draft",
|
||||
stat: "0",
|
||||
icon: DocumentIcon,
|
||||
link: "/documents?filter=DRAFT",
|
||||
},
|
||||
{
|
||||
name: "Waiting for others",
|
||||
stat: "0",
|
||||
icon: UsersIcon,
|
||||
link: "/documents?filter=PENDING",
|
||||
},
|
||||
{
|
||||
name: "Completed",
|
||||
stat: "0",
|
||||
icon: CheckBadgeIcon,
|
||||
link: "/documents?filter=COMPLETED",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Dashboard | Documenso</title>
|
||||
</Head>
|
||||
|
||||
<div className="py-10 max-sm:px-4">
|
||||
<header>
|
||||
<h1 className="text-3xl font-bold leading-tight tracking-tight text-gray-900">
|
||||
Dashboard
|
||||
</h1>
|
||||
</header>
|
||||
<dl className="mt-8 grid gap-5 md:grid-cols-3 ">
|
||||
{stats.map((item) => (
|
||||
<Link href={item.link} key={item.name}>
|
||||
<div className="overflow-hidden rounded-lg bg-white px-4 py-3 shadow hover:shadow-lg duration-300 sm:py-5 md:p-6">
|
||||
<dt className="truncate text-sm font-medium text-gray-700 ">
|
||||
<item.icon
|
||||
className="text-neon-600 mr-3 inline h-5 w-5 flex-shrink-0 sm:h-6 sm:w-6"
|
||||
aria-hidden="true"></item.icon>
|
||||
{item.name}
|
||||
</dt>
|
||||
<dd className="mt-3 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
|
||||
{getStat(item.name, props)}
|
||||
</dd>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</dl>
|
||||
|
||||
<div className="mt-12">
|
||||
<input
|
||||
id="fileUploadHelper"
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
onChange={(event: ChangeEvent) => {
|
||||
uploadDocument(event);
|
||||
}}
|
||||
hidden
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (hasSubscription) {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}
|
||||
}}
|
||||
aria-disabled={!hasSubscription}
|
||||
className="group hover:border-neon-600 duration-200 relative block w-full cursor-pointer rounded-lg border-2 border-dashed border-gray-300 p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 aria-disabled:opacity-50 aria-disabled:pointer-events-none">
|
||||
|
||||
<svg
|
||||
className="mx-auto h-12 w-12 text-gray-400 group-hover:text-gray-700 duration-200"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
viewBox="0 00 20 25"
|
||||
aria-hidden="true">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span id="add_document" className="text-gray-500 group-hover:text-neon-700 mt-2 block text-sm font-medium duration-200">
|
||||
Add a new PDF document.
|
||||
</span>
|
||||
</div>
|
||||
<ReactTooltip
|
||||
anchorId="add_document"
|
||||
place="bottom"
|
||||
content="No preparation needed. Any PDF will do."
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DashboardPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <Layout>{page}</Layout>;
|
||||
};
|
||||
|
||||
function getStat(name: string, props: any) {
|
||||
if (name === "Draft") return props.dashboard.drafts;
|
||||
if (name === "Waiting for others") return props.dashboard.waiting;
|
||||
if (name === "Completed") return props.dashboard.completed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const user = await getUserFromToken(context.req, context.res);
|
||||
if (!user)
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const documents: any[] = await getDocumentsForUserFromToken(context);
|
||||
|
||||
const drafts: PrismaDocument[] = documents.filter((d) => d.status === DocumentStatus.DRAFT);
|
||||
|
||||
const waiting: any[] = documents.filter(
|
||||
(e) =>
|
||||
e.Recipient.length > 0 &&
|
||||
e.Recipient.some((r: any) => r.sendStatus === SendStatus.SENT) &&
|
||||
e.Recipient.some((r: any) => r.signingStatus === SigningStatus.NOT_SIGNED)
|
||||
);
|
||||
|
||||
const completed: PrismaDocument[] = documents.filter(
|
||||
(d) => d.status === DocumentStatus.COMPLETED
|
||||
);
|
||||
|
||||
return {
|
||||
props: {
|
||||
dashboard: {
|
||||
drafts: drafts.length,
|
||||
waiting: waiting.length,
|
||||
completed: completed.length,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
||||
@ -1,437 +0,0 @@
|
||||
import { ReactElement, useEffect, useState } from "react";
|
||||
import { NextPageContext } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { uploadDocument } from "@documenso/features";
|
||||
import { deleteDocument, getDocuments } from "@documenso/lib/api";
|
||||
import { useSubscription } from "@documenso/lib/stripe";
|
||||
import { Button, IconButton, SelectBox } from "@documenso/ui";
|
||||
import Layout from "../components/layout";
|
||||
import type { NextPageWithLayout } from "./_app";
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
CheckBadgeIcon,
|
||||
CheckIcon,
|
||||
DocumentPlusIcon,
|
||||
EnvelopeIcon,
|
||||
FunnelIcon,
|
||||
PencilSquareIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { DocumentStatus } from "@prisma/client";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
|
||||
const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
const router = useRouter();
|
||||
const { hasSubscription } = useSubscription();
|
||||
const [documents, setDocuments]: any[] = useState([]);
|
||||
const [filteredDocuments, setFilteredDocuments] = useState([]);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
type statusFilterType = {
|
||||
label: string;
|
||||
value: DocumentStatus | "ALL";
|
||||
};
|
||||
|
||||
const statusFilters: statusFilterType[] = [
|
||||
{ label: "All", value: "ALL" },
|
||||
{ label: "Draft", value: "DRAFT" },
|
||||
{ label: "Waiting for others", value: "PENDING" },
|
||||
{ label: "Completed", value: "COMPLETED" },
|
||||
];
|
||||
const createdFilter = [
|
||||
{ label: "All Time", value: -1 },
|
||||
{ label: "Last 24 hours", value: 1 },
|
||||
{ label: "Last 7 days", value: 7 },
|
||||
{ label: "Last 30 days", value: 30 },
|
||||
{ label: "Last 3 months", value: 90 },
|
||||
{ label: "Last 12 months", value: 366 },
|
||||
];
|
||||
|
||||
const [selectedStatusFilter, setSelectedStatusFilter] = useState(statusFilters[0]);
|
||||
const [selectedCreatedFilter, setSelectedCreatedFilter] = useState(createdFilter[0]);
|
||||
|
||||
const loadDocuments = async () => {
|
||||
if (!documents.length) setLoading(true);
|
||||
getDocuments().then((res: any) => {
|
||||
res.json().then((j: any) => {
|
||||
setDocuments(j);
|
||||
setLoading(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadDocuments().finally(() => {
|
||||
setSelectedStatusFilter(
|
||||
statusFilters.filter((status) => status.value === props.filter.toUpperCase())[0]
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredDocuments(filterDocumentes(documents));
|
||||
}, [documents, selectedStatusFilter, selectedCreatedFilter]);
|
||||
|
||||
function showDocument(documentId: number) {
|
||||
router.push(`/documents/${documentId}/recipients`);
|
||||
}
|
||||
|
||||
function filterDocumentes(documents: []): any {
|
||||
let filteredDocuments = documents.filter(
|
||||
(d: any) => d.status === selectedStatusFilter.value || selectedStatusFilter.value === "ALL"
|
||||
);
|
||||
|
||||
filteredDocuments = filteredDocuments.filter((document: any) =>
|
||||
wasXDaysAgoOrLess(new Date(document.created), selectedCreatedFilter.value)
|
||||
);
|
||||
|
||||
return filteredDocuments;
|
||||
}
|
||||
|
||||
function handleStatusFilterChange(status: statusFilterType) {
|
||||
router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: { filter: status.value },
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
shallow: true, // Perform a shallow update, without reloading the page
|
||||
}
|
||||
);
|
||||
setSelectedStatusFilter(status);
|
||||
}
|
||||
|
||||
function wasXDaysAgoOrLess(documentDate: Date, lastXDays: number): boolean {
|
||||
if (lastXDays < 0) return true;
|
||||
|
||||
const millisecondsInDay = 24 * 60 * 60 * 1000; // Number of milliseconds in a day
|
||||
const today: Date = new Date(); // Today's date
|
||||
|
||||
// Calculate the difference between the two dates in days
|
||||
const diffInDays = Math.floor((today.getTime() - documentDate.getTime()) / millisecondsInDay);
|
||||
|
||||
console.log(diffInDays);
|
||||
|
||||
// Check if the difference is letss or equal to lastXDays
|
||||
return diffInDays <= lastXDays;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Documents | Documenso</title>
|
||||
</Head>
|
||||
<div className="px-4 sm:px-6 lg:px-8">
|
||||
<div className="mt-10 sm:flex sm:items-center">
|
||||
<div className="sm:flex-auto">
|
||||
<header>
|
||||
<h1 className="text-3xl font-bold leading-tight tracking-tight text-gray-900">
|
||||
Documents
|
||||
</h1>
|
||||
</header>
|
||||
</div>
|
||||
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<Button
|
||||
icon={DocumentPlusIcon}
|
||||
disabled={!hasSubscription}
|
||||
onClick={() => {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}}>
|
||||
Add Document
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 mb-12 flex flex-wrap items-center justify-start gap-x-4 md:justify-end gap-y-4">
|
||||
<SelectBox
|
||||
className="block flex-1 md:flex-none md:w-1/4"
|
||||
label="Status"
|
||||
options={statusFilters}
|
||||
value={selectedStatusFilter}
|
||||
onChange={handleStatusFilterChange}
|
||||
/>
|
||||
<SelectBox
|
||||
className="block flex-1 md:flex-none md:w-1/4"
|
||||
label="Created"
|
||||
options={createdFilter}
|
||||
value={selectedCreatedFilter}
|
||||
onChange={setSelectedCreatedFilter}
|
||||
/>
|
||||
<div className="block w-fit pt-5">
|
||||
{filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 max-w-[1100px]" hidden={!loading}>
|
||||
<div className="ph-item">
|
||||
<div className="ph-col-12">
|
||||
<div className="ph-picture"></div>
|
||||
<div className="ph-row">
|
||||
<div className="ph-col-6 big"></div>
|
||||
<div className="ph-col-4 empty big"></div>
|
||||
<div className="ph-col-2 big"></div>
|
||||
<div className="ph-col-4"></div>
|
||||
<div className="ph-col-8 empty"></div>
|
||||
<div className="ph-col-6"></div>
|
||||
<div className="ph-col-6 empty"></div>
|
||||
<div className="ph-col-12"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex flex-col" hidden={!documents.length || loading}>
|
||||
<div
|
||||
className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8"
|
||||
hidden={!documents.length || loading}>
|
||||
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
|
||||
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-300">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||
Title
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||
Recipients
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||
Created
|
||||
</th>
|
||||
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span className="sr-only">Delete</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
{filteredDocuments.map((document: any, index: number) => (
|
||||
<tr
|
||||
key={document.id}
|
||||
className="cursor-pointer hover:bg-gray-100"
|
||||
onClick={(event) => showDocument(document.id)}>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
{document.title || "#" + document.id}
|
||||
</td>
|
||||
<td className="inline-flex max-w-[250px] flex-wrap gap-x-2 gap-y-1 whitespace-nowrap py-3 text-sm text-gray-500">
|
||||
{document.Recipient.map((item: any) => (
|
||||
<div key={item.id}>
|
||||
{item.sendStatus === "NOT_SENT" ? (
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="inline-flex h-6 flex-shrink-0 items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
|
||||
<span id="sent_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="inline-flex h-6 flex-shrink-0 items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
||||
<EnvelopeIcon className="mr-1 inline h-4"></EnvelopeIcon>
|
||||
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{item.readStatus === "OPENED" &&
|
||||
item.signingStatus === "NOT_SIGNED" ? (
|
||||
<span id="read_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="inline-flex h-6 flex-shrink-0 items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
||||
<CheckIcon className="-mr-2 inline h-4"></CheckIcon>
|
||||
<CheckIcon className="mr-1 inline h-4"></CheckIcon>
|
||||
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{item.signingStatus === "SIGNED" ? (
|
||||
<span id="signed_icon">
|
||||
<span className="inline-flex h-6 flex-shrink-0 items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>{" "}
|
||||
{item.email}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{document.Recipient.length === 0 ? "-" : null}
|
||||
<ReactTooltip
|
||||
anchorId="sent_icon"
|
||||
place="bottom"
|
||||
content="Document was sent to recipient."
|
||||
/>
|
||||
<ReactTooltip
|
||||
anchorId="read_icon"
|
||||
place="bottom"
|
||||
content="Document was opened but not signed yet."
|
||||
/>
|
||||
<ReactTooltip
|
||||
anchorId="signed_icon"
|
||||
place="bottom"
|
||||
content="Document was signed by the recipient."
|
||||
/>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
{formatDocumentStatus(document.status)}
|
||||
<p>
|
||||
<small hidden={document.Recipient.length === 0}>
|
||||
{document.Recipient.filter((r: any) => r.signingStatus === "SIGNED")
|
||||
.length || 0}
|
||||
/{document.Recipient.length || 0}
|
||||
</small>
|
||||
</p>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
{new Date(document.created).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||
<div>
|
||||
<IconButton
|
||||
icon={PencilSquareIcon}
|
||||
className="mr-2"
|
||||
onClick={(event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
router.push("/documents/" + document.id);
|
||||
}}
|
||||
disabled={document.status === "COMPLETED"}
|
||||
/>
|
||||
<IconButton
|
||||
icon={ArrowDownTrayIcon}
|
||||
className="mr-2"
|
||||
onClick={(event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
router.push("/api/documents/" + document.id);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={TrashIcon}
|
||||
onClick={(event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (confirm("Are you sure you want to delete this document")) {
|
||||
const documentsWithoutIndex = [...documents];
|
||||
const removedItem: any = documentsWithoutIndex.splice(index, 1);
|
||||
setDocuments(documentsWithoutIndex);
|
||||
deleteDocument(document.id)
|
||||
.catch((err) => {
|
||||
documentsWithoutIndex.splice(index, 0, removedItem);
|
||||
setDocuments(documentsWithoutIndex);
|
||||
})
|
||||
.then(() => {
|
||||
loadDocuments();
|
||||
});
|
||||
}
|
||||
}}></IconButton>
|
||||
<span className="sr-only">, {document.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div hidden={filteredDocuments.length > 0} className="mx-auto mt-12 w-fit p-3">
|
||||
<FunnelIcon className="mr-1 inline w-5 align-middle" /> Nothing here. Maybe try a
|
||||
different filter.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-24 text-center" id="empty" hidden={documents.length > 0 || loading}>
|
||||
<svg
|
||||
className="mx-auto h-12 w-12 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900">No documents</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Get started by adding a document. Any PDF will do.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
icon={PlusIcon}
|
||||
disabled={!hasSubscription}
|
||||
onClick={() => {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}}>
|
||||
Add Document
|
||||
</Button>
|
||||
<input
|
||||
id="fileUploadHelper"
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
onChange={(event: any) => {
|
||||
uploadDocument(event);
|
||||
}}
|
||||
hidden
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ReactTooltip
|
||||
anchorId="empty"
|
||||
place="bottom"
|
||||
content="No preparation needed. Any PDF will do."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function formatDocumentStatus(status: DocumentStatus) {
|
||||
switch (status) {
|
||||
case DocumentStatus.DRAFT:
|
||||
return "Draft";
|
||||
|
||||
case DocumentStatus.PENDING:
|
||||
return "Waiting for others";
|
||||
|
||||
case DocumentStatus.COMPLETED:
|
||||
return "Completed";
|
||||
}
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
const filter = context.query["filter"];
|
||||
|
||||
return {
|
||||
props: {
|
||||
filter: filter || "ALL",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
DocumentsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <Layout>{page}</Layout>;
|
||||
};
|
||||
|
||||
export default DocumentsPage;
|
||||
@ -1,130 +0,0 @@
|
||||
import { ReactElement } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import { useSubscription } from "@documenso/lib/stripe";
|
||||
import { Breadcrumb, Button } from "@documenso/ui";
|
||||
import PDFEditor from "../../../components/editor/pdf-editor";
|
||||
import Layout from "../../../components/layout";
|
||||
import { NextPageWithLayout } from "../../_app";
|
||||
import { InformationCircleIcon, PaperAirplaneIcon, UsersIcon } from "@heroicons/react/24/outline";
|
||||
import { DocumentStatus } from "@prisma/client";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
|
||||
const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
|
||||
const router = useRouter();
|
||||
const { hasSubscription } = useSubscription();
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div>
|
||||
<div>
|
||||
<Breadcrumb
|
||||
document={props.document}
|
||||
items={[
|
||||
{
|
||||
title: "Documents",
|
||||
href: "/documents",
|
||||
},
|
||||
{
|
||||
title: props.document.title,
|
||||
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 md:flex md:items-center md:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{props.document.title}
|
||||
</h2>
|
||||
<div className="mt-1 flex flex-col sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-6">
|
||||
<div className="mt-2 flex items-center text-sm text-gray-500">
|
||||
<UsersIcon
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<Link href={`/documents/${props.document.id}/recipients`}>
|
||||
{props?.document?.Recipient?.length} Recipients
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center text-sm text-gray-500">
|
||||
<InformationCircleIcon
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{formatDocumentStatus(props.document.status)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-shrink-0 md:mt-0 md:ml-4">
|
||||
<Button
|
||||
icon={PaperAirplaneIcon}
|
||||
className="ml-3"
|
||||
href={NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients"}
|
||||
onClick={() => {
|
||||
if (
|
||||
confirm(`Send document out to ${props?.document?.Recipient?.length} recipients?`)
|
||||
) {
|
||||
}
|
||||
}}>
|
||||
Prepare to Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto w-fit">
|
||||
<PDFEditor document={props.document} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function formatDocumentStatus(status: DocumentStatus) {
|
||||
switch (status) {
|
||||
case DocumentStatus.DRAFT:
|
||||
return "Draft";
|
||||
|
||||
case DocumentStatus.PENDING:
|
||||
return "Waiting for others";
|
||||
|
||||
case DocumentStatus.COMPLETED:
|
||||
return "Completed";
|
||||
}
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const user = await getUserFromToken(context.req, context.res);
|
||||
if (!user)
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const { id: documentId } = context.query;
|
||||
|
||||
try {
|
||||
const document: PrismaDocument = await getDocument(+documentId, context.req, context.res);
|
||||
|
||||
return {
|
||||
props: {
|
||||
document: JSON.parse(JSON.stringify({ ...document, document: "" })),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
DocumentsDetailPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <Layout>{page}</Layout>;
|
||||
};
|
||||
|
||||
export default DocumentsDetailPage;
|
||||
@ -1,376 +0,0 @@
|
||||
import { ReactElement, useRef, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL, classNames } from "@documenso/lib";
|
||||
import { createOrUpdateRecipient, deleteRecipient, sendSigningRequests } from "@documenso/lib/api";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import { Breadcrumb, Button, Dialog, IconButton, Tooltip } from "@documenso/ui";
|
||||
import Layout from "../../../components/layout";
|
||||
import { NextPageWithLayout } from "../../_app";
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
CheckBadgeIcon,
|
||||
CheckIcon,
|
||||
EnvelopeIcon,
|
||||
PaperAirplaneIcon,
|
||||
PencilSquareIcon,
|
||||
TrashIcon,
|
||||
UserPlusIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { DocumentStatus, Document as PrismaDocument, Recipient } from "@prisma/client";
|
||||
import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useSubscription } from "@documenso/lib/stripe";
|
||||
|
||||
export type FormValues = {
|
||||
signers: Array<Pick<Recipient, 'id' | 'email' | 'name' | 'sendStatus' | 'readStatus' | 'signingStatus'>>;
|
||||
};
|
||||
|
||||
type FormSigner = FormValues["signers"][number];
|
||||
|
||||
const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
const { hasSubscription } = useSubscription();
|
||||
const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso";
|
||||
const breadcrumbItems = [
|
||||
{
|
||||
title: "Documents",
|
||||
href: "/documents",
|
||||
},
|
||||
{
|
||||
title: props.document.title,
|
||||
href:
|
||||
props.document.status !== DocumentStatus.COMPLETED
|
||||
? NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id
|
||||
: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients",
|
||||
},
|
||||
{
|
||||
title: "Recipients",
|
||||
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients",
|
||||
},
|
||||
];
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues: { signers: props?.document?.Recipient },
|
||||
});
|
||||
const {
|
||||
register,
|
||||
trigger,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = form;
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
keyName: "dieldArrayId",
|
||||
name: "signers",
|
||||
control,
|
||||
});
|
||||
const formValues = useWatch({ control, name: "signers" });
|
||||
const cancelButtonRef = useRef(null);
|
||||
const hasEmailError = (formValue: FormSigner): boolean => {
|
||||
const index = formValues.findIndex((e) => e.id === formValue.id);
|
||||
return !!errors?.signers?.[index]?.email;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<div className="mt-10 px-6 sm:px-0">
|
||||
<div>
|
||||
<Breadcrumb document={props.document} items={breadcrumbItems} />
|
||||
</div>
|
||||
<div className="mt-2 md:flex md:items-center md:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{props.document.title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-shrink-0 md:mt-0 md:ml-4">
|
||||
<Button
|
||||
icon={ArrowDownTrayIcon}
|
||||
color="secondary"
|
||||
className="mr-2"
|
||||
href={"/api/documents/" + props.document.id}>
|
||||
Download
|
||||
</Button>
|
||||
{props.document.status !== DocumentStatus.COMPLETED && (
|
||||
<>
|
||||
<Button
|
||||
icon={PencilSquareIcon}
|
||||
disabled={props.document.status === DocumentStatus.COMPLETED}
|
||||
color={
|
||||
props.document.status === DocumentStatus.COMPLETED ? "primary" : "secondary"
|
||||
}
|
||||
className="mr-2"
|
||||
href={breadcrumbItems[1].href}>
|
||||
Edit Document
|
||||
</Button>
|
||||
<Button
|
||||
className="min-w-[125px]"
|
||||
color="primary"
|
||||
icon={PaperAirplaneIcon}
|
||||
onClick={() => {
|
||||
formValues.some((r) => r.email && hasEmailError(r))
|
||||
? toast.error("Please enter a valid email address.", { id: "invalid email" })
|
||||
: setOpen(true);
|
||||
}}
|
||||
disabled={
|
||||
!hasSubscription ||
|
||||
(formValues.length || 0) === 0 ||
|
||||
!formValues.some(
|
||||
(r) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
|
||||
) ||
|
||||
loading
|
||||
}>
|
||||
Send
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10 overflow-hidden rounded-md bg-white p-4 shadow sm:p-6">
|
||||
<div className="border-b border-gray-200 pb-3 sm:pb-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900 ">Signers</h3>
|
||||
<p className="mt-2 max-w-4xl text-sm text-gray-500">
|
||||
{props.document.status !== DocumentStatus.COMPLETED
|
||||
? "The people who will sign the document."
|
||||
: "The people who signed the document."}
|
||||
</p>
|
||||
</div>
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onChange={() => {
|
||||
trigger();
|
||||
}}>
|
||||
<ul role="list" className="divide-y divide-gray-200">
|
||||
{fields.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="group w-full border-0 px-2 py-3 hover:bg-green-50 sm:py-4">
|
||||
<div id="container" className="block w-full lg:flex lg:justify-between">
|
||||
<div className="block space-y-2 md:flex md:space-x-2 md:space-y-0">
|
||||
<div
|
||||
className={classNames(
|
||||
"focus-within:border-neon focus-within:ring-neon rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:ring-1 md:w-[250px]",
|
||||
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
||||
)}>
|
||||
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
{...register(`signers.${index}.email`, {
|
||||
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
})}
|
||||
defaultValue={item.email}
|
||||
disabled={item.sendStatus === "SENT" || loading}
|
||||
onBlur={() => {
|
||||
if (!errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event: any) => {
|
||||
if (event.key === "Enter")
|
||||
if (!errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
className="block w-full border-0 bg-inherit p-0 text-gray-900 placeholder-gray-500 outline-none disabled:bg-neutral-100 sm:text-sm"
|
||||
/>
|
||||
{errors?.signers?.[index] ? (
|
||||
<p className="mt-2 text-sm text-red-600" id="email-error">
|
||||
<XMarkIcon className="inline h-5" /> Invalid Email
|
||||
</p>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"focus-within:border-neon focus-within:ring-neon rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:ring-1 md:w-[250px]",
|
||||
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
||||
)}>
|
||||
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
|
||||
Name (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register(`signers.${index}.name`)}
|
||||
defaultValue={item.name}
|
||||
disabled={item.sendStatus === "SENT" || loading}
|
||||
onBlur={() => {
|
||||
if (!errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event: any) => {
|
||||
if (event.key === "Enter" && !errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
className="block w-full border-0 bg-inherit p-0 text-gray-900 placeholder-gray-500 outline-none disabled:bg-neutral-100 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 lg:ml-2">
|
||||
<div className="mb-2 mr-2 flex lg:mr-0">
|
||||
<div key={item.id} className="space-x-2">
|
||||
{item.sendStatus === "NOT_SENT" ? (
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800">
|
||||
Not Sent
|
||||
</span>
|
||||
) : null}
|
||||
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
|
||||
<span id="sent_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800 ">
|
||||
<CheckIcon className="mr-1 inline h-5" /> Sent
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
{item.readStatus === "OPENED" && item.signingStatus === "NOT_SIGNED" ? (
|
||||
<span id="read_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800">
|
||||
<CheckIcon className="-mr-2 inline h-5"></CheckIcon>
|
||||
<CheckIcon className="mr-1 inline h-5"></CheckIcon>
|
||||
Seen
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
{item.signingStatus === "SIGNED" ? (
|
||||
<span id="signed_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="mt-3 inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>
|
||||
Signed
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{props.document.status !== DocumentStatus.COMPLETED && (
|
||||
<div className="mr-1 flex">
|
||||
<Tooltip label="Resend">
|
||||
<IconButton
|
||||
icon={PaperAirplaneIcon}
|
||||
disabled={
|
||||
!item.id ||
|
||||
item.sendStatus !== "SENT" ||
|
||||
item.signingStatus === "SIGNED" ||
|
||||
loading
|
||||
}
|
||||
onClick={(event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (confirm("Resend this signing request?")) {
|
||||
setLoading(true);
|
||||
sendSigningRequests(props.document, [item.id]).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="mx-1 group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete">
|
||||
<IconButton
|
||||
icon={TrashIcon}
|
||||
disabled={!item.id || item.sendStatus === "SENT" || loading}
|
||||
onClick={(event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (confirm("Delete this signing request?")) {
|
||||
const removedItem = { ...fields }[index];
|
||||
remove(index);
|
||||
deleteRecipient(item)?.catch((err) => {
|
||||
append(removedItem);
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="mx-1 group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{props.document.status !== "COMPLETED" && (
|
||||
<Button
|
||||
icon={UserPlusIcon}
|
||||
className="mt-3"
|
||||
onClick={() => {
|
||||
createOrUpdateRecipient({
|
||||
id: "",
|
||||
email: "",
|
||||
name: "",
|
||||
documentId: props.document.id,
|
||||
}).then((res) => {
|
||||
append(res);
|
||||
});
|
||||
}}>
|
||||
Add Signer
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
title="Ready to send"
|
||||
document={props.document}
|
||||
formValues={formValues}
|
||||
open={open}
|
||||
setLoading={setLoading}
|
||||
setOpen={setOpen}
|
||||
icon={<EnvelopeIcon className="h-6 w-6 text-green-600" aria-hidden="true" />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
RecipientsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <Layout>{page}</Layout>;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const user = await getUserFromToken(context.req, context.res);
|
||||
if (!user)
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const { id: documentId } = context.query;
|
||||
const document: PrismaDocument = await getDocument(+documentId, context.req, context.res);
|
||||
|
||||
return {
|
||||
props: {
|
||||
document: JSON.parse(JSON.stringify({ ...document, document: "" })),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default RecipientsPage;
|
||||
@ -1,120 +0,0 @@
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import prisma from "@documenso/prisma";
|
||||
import PDFSigner from "../../../components/editor/pdf-signer";
|
||||
import { NextPageWithLayout } from "../../_app";
|
||||
import { ClockIcon } from "@heroicons/react/24/outline";
|
||||
import { ReadStatus } from "@prisma/client";
|
||||
import { DocumentStatus, FieldType } from "@prisma/client";
|
||||
|
||||
const SignPage: NextPageWithLayout = (props: any) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Sign | Documenso</title>
|
||||
</Head>
|
||||
{!props.expired ? (
|
||||
<PDFSigner document={props.document} recipient={props.recipient} fields={props.fields} />
|
||||
) : (
|
||||
<>
|
||||
<div className="mx-auto w-fit px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
|
||||
<ClockIcon className="text-neon mr-1 inline w-10"></ClockIcon>
|
||||
<h1 className="text-neon inline align-middle text-base font-medium">Time flies.</h1>
|
||||
<p className="mt-2 text-4xl font-bold tracking-tight">This signing link is expired.</p>
|
||||
<p className="mt-2 text-base text-gray-500">
|
||||
Please ask {props.document.User.name ? `${props.document.User.name}` : `the sender`}{" "}
|
||||
to resend it.
|
||||
</p>
|
||||
<div className="mx-auto w-fit pt-20 text-xl"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="relative mx-96">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 text-center text-sm text-gray-600">
|
||||
Want to send of your own?{" "}
|
||||
<Link href="/signup?source=expired" className="text-neon hover:text-neon font-medium">
|
||||
Create your own Account
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const recipientToken: string = context.query["token"];
|
||||
|
||||
await prisma.recipient.updateMany({
|
||||
where: {
|
||||
token: recipientToken,
|
||||
},
|
||||
data: {
|
||||
readStatus: ReadStatus.OPENED,
|
||||
},
|
||||
});
|
||||
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
token: recipientToken,
|
||||
},
|
||||
include: {
|
||||
Document: { include: { User: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/404",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Document is already signed
|
||||
if (recipient.Document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `/documents/${recipient.Document.id}/signed?token=${recipientToken}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Clean up potential unsigned free place fields from UI from previous page visits
|
||||
await prisma.field.deleteMany({
|
||||
where: {
|
||||
type: { in: [FieldType.FREE_SIGNATURE] },
|
||||
Signature: { is: null },
|
||||
},
|
||||
});
|
||||
|
||||
const unsignedFields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId: recipient.Document.id,
|
||||
recipientId: recipient.id,
|
||||
Signature: { is: null },
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
Signature: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
recipient: JSON.parse(JSON.stringify(recipient)),
|
||||
document: JSON.parse(JSON.stringify({ ...recipient.Document, document: "" })),
|
||||
fields: JSON.parse(JSON.stringify(unsignedFields)),
|
||||
expired: recipient.expired ? new Date(recipient.expired) < new Date() : false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default SignPage;
|
||||
@ -1,96 +0,0 @@
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { Button, IconButton } from "@documenso/ui";
|
||||
import { NextPageWithLayout } from "../../_app";
|
||||
import { ArrowDownTrayIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
|
||||
import { truncate } from "@documenso/lib/helpers";
|
||||
|
||||
const Signed: NextPageWithLayout = (props: any) => {
|
||||
const router = useRouter();
|
||||
const allRecipientsSigned = props.document.Recipient?.every(
|
||||
(r: any) => r.signingStatus === "SIGNED"
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Sign | Documenso</title>
|
||||
</Head>
|
||||
<div className="mx-auto w-fit px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
|
||||
<CheckBadgeIcon className="text-neon mr-1 inline w-10"></CheckBadgeIcon>
|
||||
<h1 className="text-neon inline align-middle text-base font-medium">It's done!</h1>
|
||||
<p className="mt-2 text-4xl font-bold tracking-tight">
|
||||
You signed "{truncate(props.document.title)}"
|
||||
</p>
|
||||
<p className="mt-2 max-w-sm text-base text-gray-500" hidden={allRecipientsSigned}>
|
||||
You will be notfied when all recipients have signed.
|
||||
</p>
|
||||
<p className="mt-2 max-w-sm text-base text-gray-500" hidden={!allRecipientsSigned}>
|
||||
All recipients signed.
|
||||
</p>
|
||||
<div className="mx-auto w-fit pt-20 text-xl" hidden={!allRecipientsSigned}>
|
||||
<Button
|
||||
icon={ArrowDownTrayIcon}
|
||||
color="secondary"
|
||||
onClick={(event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
router.push(
|
||||
"/api/documents/" + props.document.id + "?token=" + props.recipient.token
|
||||
);
|
||||
}}>
|
||||
Download "{props.document.title}"
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="relative mx-96">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 text-center text-sm text-gray-600">
|
||||
Want to send slick signing links like this one?{" "}
|
||||
<Link href="https://documenso.com" className="text-neon hover:text-neon font-medium">
|
||||
Hosted Documenso is here!
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const recipientToken: string = context.query["token"];
|
||||
|
||||
const recipient = await prisma.recipient.findFirstOrThrow({
|
||||
where: {
|
||||
token: recipientToken,
|
||||
},
|
||||
include: {
|
||||
Document: { include: { Recipient: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId: recipient.Document.id,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
document: JSON.parse(JSON.stringify(recipient.Document)),
|
||||
fields: JSON.parse(JSON.stringify(fields)),
|
||||
recipient: JSON.parse(JSON.stringify(recipient)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default Signed;
|
||||
@ -1,18 +0,0 @@
|
||||
import { NextPageContext } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
|
||||
function RedirectPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
const session = await getSession(context);
|
||||
|
||||
if (!session?.user?.email) {
|
||||
return { redirect: { permanent: false, destination: "/login" } };
|
||||
}
|
||||
|
||||
return { redirect: { permanent: false, destination: "/dashboard" } };
|
||||
}
|
||||
|
||||
export default RedirectPage;
|
||||
@ -1,34 +0,0 @@
|
||||
import Head from "next/head";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import Login from "../components/login";
|
||||
|
||||
export default function LoginPage(props: any) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Login | Documenso</title>
|
||||
</Head>
|
||||
<Login allowSignup={props.ALLOW_SIGNUP}></Login>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const user = await getUserFromToken(context.req, context.res);
|
||||
if (user)
|
||||
return {
|
||||
redirect: {
|
||||
source: "/login",
|
||||
destination: "/dashboard",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const ALLOW_SIGNUP = process.env.NEXT_PUBLIC_ALLOW_SIGNUP === "true";
|
||||
|
||||
return {
|
||||
props: {
|
||||
ALLOW_SIGNUP,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
import SettingsPage from ".";
|
||||
|
||||
export default SettingsPage;
|
||||
@ -1 +0,0 @@
|
||||
export { default } from ".";
|
||||
@ -1,14 +0,0 @@
|
||||
import type { ReactElement } from "react";
|
||||
import Layout from "../../components/layout";
|
||||
import Settings from "../../components/settings";
|
||||
import type { NextPageWithLayout } from "../_app";
|
||||
|
||||
const SettingsPage: NextPageWithLayout = () => {
|
||||
return <Settings></Settings>;
|
||||
};
|
||||
|
||||
SettingsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <Layout>{page}</Layout>;
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
@ -1,3 +0,0 @@
|
||||
import SettingsPage from ".";
|
||||
|
||||
export default SettingsPage;
|
||||
@ -1,3 +0,0 @@
|
||||
import SettingsPage from ".";
|
||||
|
||||
export default SettingsPage;
|
||||
@ -1,42 +0,0 @@
|
||||
import { NextPageContext } from "next";
|
||||
import Head from "next/head";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import Signup from "../components/signup";
|
||||
|
||||
export default function SignupPage(props: { source: string }) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Signup | Documenso</title>
|
||||
</Head>
|
||||
<Signup source={props.source}></Signup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
if (process.env.NEXT_PUBLIC_ALLOW_SIGNUP !== "true")
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const user = await getUserFromToken(context.req, context.res);
|
||||
if (user)
|
||||
return {
|
||||
redirect: {
|
||||
source: "/signup",
|
||||
destination: "/dashboard",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const signupSource: string = context.query["source"];
|
||||
return {
|
||||
props: {
|
||||
source: signupSource ? signupSource : "",
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user