Merge branch 'main' into doc-162

This commit is contained in:
Timur Ercan
2023-03-21 14:16:47 +01:00
17 changed files with 178 additions and 64 deletions

View File

@ -18,4 +18,8 @@ NEXTAUTH_URL='http://localhost:3000'
# You can also configure you own SMTP server using Nodemailer in sendMailts. (currently not possible via config) # You can also configure you own SMTP server using Nodemailer in sendMailts. (currently not possible via config)
SENDGRID_API_KEY='' SENDGRID_API_KEY=''
# Sender for signing requests and completion mails. # Sender for signing requests and completion mails.
MAIL_FROM='' MAIL_FROM=''
#FEATURE FLAGS
# Allow users to register via the /signup page. Otherwise they will be redirect to the home page.
ALLOW_SIGNUP=true

View File

@ -10,7 +10,7 @@ If you plan to contribute to Documenso, please take a moment to feel awesome ✨
## Developing ## Developing
The development branch is <code>development</code>. All pull request should be made against this branch. If you need help getting started, [join us on Slack](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w). The development branch is <code>main</code>. All pull request should be made against this branch. If you need help getting started, [join us on Slack](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w).
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
own GitHub account and then own GitHub account and then

View File

@ -173,7 +173,11 @@ export default function PDFSigner(props: any) {
FieldType.FREE_SIGNATURE FieldType.FREE_SIGNATURE
); );
createOrUpdateField(props.document, freeSignatureField).then((res) => { createOrUpdateField(
props.document,
freeSignatureField,
recipient.token
).then((res) => {
setFields((prevState) => [...prevState, res]); setFields((prevState) => [...prevState, res]);
setDialogField(res); setDialogField(res);
setOpen(true); setOpen(true);

View File

@ -3,6 +3,7 @@ import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
import EditableField from "./editable-field"; import EditableField from "./editable-field";
import SignableField from "./signable-field"; import SignableField from "./signable-field";
import short from "short-uuid"; import short from "short-uuid";
import { FieldType } from "@prisma/client";
export default function PDFViewer(props) { export default function PDFViewer(props) {
const [numPages, setNumPages] = useState(null); const [numPages, setNumPages] = useState(null);
@ -71,21 +72,25 @@ export default function PDFViewer(props) {
onRenderError={() => setLoading(false)} onRenderError={() => setLoading(false)}
></Page> ></Page>
{props?.fields {props?.fields
.filter((item) => item.page === index) .filter((field) => field.page === index)
.map((item) => .map((field) =>
props.readonly ? ( props.readonly ? (
<SignableField <SignableField
onClick={props.onClick} onClick={props.onClick}
key={item.id} key={field.id}
field={item} field={field}
className="absolute" className="absolute"
onDelete={onDeleteHandler} onDelete={onDeleteHandler}
></SignableField> ></SignableField>
) : ( ) : (
<EditableField <EditableField
hidden={item.Signature || item.inserted} hidden={
key={item.id} field.Signature ||
field={item} field.inserted ||
field.type === FieldType.FREE_SIGNATURE
}
key={field.id}
field={field}
className="absolute" className="absolute"
onPositionChanged={onPositionChangedHandler} onPositionChanged={onPositionChangedHandler}
onDelete={onDeleteHandler} onDelete={onDeleteHandler}

View File

@ -2,6 +2,7 @@ import React, { useState } from "react";
import Draggable from "react-draggable"; import Draggable from "react-draggable";
import { IconButton } from "@documenso/ui"; import { IconButton } from "@documenso/ui";
import { XCircleIcon } from "@heroicons/react/20/solid"; import { XCircleIcon } from "@heroicons/react/20/solid";
import { classNames } from "@documenso/lib";
const stc = require("string-to-color"); const stc = require("string-to-color");
type FieldPropsType = { type FieldPropsType = {
@ -43,13 +44,19 @@ export default function SignableField(props: FieldPropsType) {
if (!field?.signature) props.onClick(props.field); if (!field?.signature) props.onClick(props.field);
}} }}
ref={nodeRef} ref={nodeRef}
className="cursor-pointer opacity-80 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none hover:brightness-50" className={classNames(
"opacity-80 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none",
field.type === "SIGNATURE"
? "cursor-pointer hover:brightness-50"
: "cursor-not-allowed"
)}
style={{ style={{
background: stc(props.field.Recipient.email), background: stc(props.field.Recipient.email),
}} }}
> >
<div hidden={field?.signature} className="font-medium my-4"> <div hidden={field?.signature} className="font-medium my-4">
{field.type === "SIGNATURE" ? "SIGN HERE" : ""} {field.type === "SIGNATURE" ? "SIGN HERE" : ""}
{field.type === "DATE" ? <small>Date (filled on sign)</small> : ""}
</div> </div>
<div <div
hidden={!field?.signature} hidden={!field?.signature}

View File

@ -17,12 +17,11 @@ interface LoginValues {
csrfToken: string; csrfToken: string;
} }
export default function Login() { export default function Login(props: any) {
const router = useRouter(); const router = useRouter();
const methods = useForm<LoginValues>(); const methods = useForm<LoginValues>();
const { register, formState } = methods; const { register, formState } = methods;
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
let callbackUrl = let callbackUrl =
typeof router.query?.callbackUrl === "string" typeof router.query?.callbackUrl === "string"
? router.query.callbackUrl ? router.query.callbackUrl
@ -117,7 +116,6 @@ export default function Login() {
/> />
</div> </div>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-sm"> <div className="text-sm">
<a href="#" className="font-medium text-neon hover:text-neon"> <a href="#" className="font-medium text-neon hover:text-neon">
@ -125,7 +123,6 @@ export default function Login() {
</a> </a>
</div> </div>
</div> </div>
<div> <div>
<Button <Button
type="submit" type="submit"
@ -152,15 +149,27 @@ export default function Login() {
<div className="relative flex justify-center"></div> <div className="relative flex justify-center"></div>
</div> </div>
</div> </div>
<p className="mt-2 text-center text-sm text-gray-600"> {props.allowSignup ? (
Are you new here?{" "} <p className="mt-2 text-center text-sm text-gray-600">
<Link Are you new here?{" "}
href="/signup" <Link
className="font-medium text-neon hover:text-neon" href="/signup"
> className="font-medium text-neon hover:text-neon"
Create a new Account >
</Link> Create a new Account
</p> </Link>
</p>
) : (
<p className="mt-2 text-center text-sm text-gray-600">
Like Documenso{" "}
<Link
href="https://documenso.com"
className="font-medium text-neon hover:text-neon"
>
Hosted Documenso will be availible soon
</Link>
</p>
)}
</form> </form>
</FormProvider> </FormProvider>
</div> </div>

View File

@ -115,9 +115,13 @@ export default function Setttings() {
</aside> </aside>
<form <form
className="divide-y divide-gray-200 lg:col-span-9" className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
action="#" action="#"
method="POST" method="POST"
hidden={
subNavigation.filter((e) => e.current)[0]?.name !==
subNavigation[0].name
}
> >
{/* Profile section */} {/* Profile section */}
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
@ -170,6 +174,26 @@ export default function Setttings() {
<Button onClick={() => updateUser(user)}>Save</Button> <Button onClick={() => updateUser(user)}>Save</Button>
</div> </div>
</form> </form>
<div
hidden={
subNavigation.filter((e) => e.current)[0]?.name !==
subNavigation[1].name
}
className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
>
{/* Passwords section */}
<div className="py-6 px-4 sm:p-6 lg:pb-8">
<div>
<h2 className="text-lg font-medium leading-6 text-gray-900">
Password
</h2>
<p className="mt-1 text-sm text-gray-500">
Forgot your passwort? Email <b>hi@documenso.com</b> to reset
it.
</p>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -18,10 +18,10 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
} }
let user = null; let user = null;
let recipient = null;
if (recipientToken) { if (recipientToken) {
// Request from signing page without login // Request from signing page without login
const recipient = await prisma.recipient.findFirst({ recipient = await prisma.recipient.findFirst({
where: { where: {
token: recipientToken?.toString(), token: recipientToken?.toString(),
}, },
@ -37,7 +37,14 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
if (!user) return res.status(401).end(); if (!user) return res.status(401).end();
const document: PrismaDocument = await getDocument(+documentId, req, res); 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) if (!document)
res.status(404).end(`No document with id ${documentId} found.`); res.status(404).end(`No document with id ${documentId} found.`);
@ -45,16 +52,18 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const signaturesCount = await prisma.signature.count({ const signaturesCount = await prisma.signature.count({
where: { where: {
Field: { Field: {
documentId: document.id, documentId: document?.id,
}, },
}, },
}); });
let signedDocumentAsBase64 = document.document; let signedDocumentAsBase64 = document?.document || "";
// No need to add a signature, if no one signed yet. // No need to add a signature, if no one signed yet.
if (signaturesCount > 0) { if (signaturesCount > 0) {
signedDocumentAsBase64 = await addDigitalSignature(document.document); signedDocumentAsBase64 = await addDigitalSignature(
document?.document || ""
);
} }
const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64"); const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64");
@ -62,7 +71,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
res.setHeader("Content-Length", buffer.length); res.setHeader("Content-Length", buffer.length);
res.setHeader( res.setHeader(
"Content-Disposition", "Content-Disposition",
`attachment; filename=${document.title}` `attachment; filename=${document?.title}`
); );
return res.status(200).send(buffer); return res.status(200).send(buffer);

View File

@ -36,8 +36,10 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
} }
async function postHandler(req: NextApiRequest, res: NextApiResponse) { async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res); const { token: recipientToken } = req.query;
const { id: documentId } = req.query; let user = null;
if (!recipientToken) user = await getUserFromToken(req, res);
if (!user && !recipientToken) return res.status(401).end();
const body: { const body: {
id: number; id: number;
type: FieldType; type: FieldType;
@ -48,18 +50,30 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
customText: string; customText: string;
} = req.body; } = req.body;
if (!user) return; const { id: documentId } = req.query;
if (!documentId) { if (!documentId) {
res.status(400).send("Missing parameter documentId."); return res.status(400).send("Missing parameter documentId.");
return;
} }
const document: PrismaDocument = await getDocument(+documentId, req, res); if (recipientToken) {
const recipient = await prisma.recipient.findFirst({
where: { token: recipientToken?.toString() },
});
// todo entity ownerships checks if (!recipient || recipient?.documentId !== +documentId)
if (document.userId !== user.id) { return res
return res.status(401).send("User does not have access to this document."); .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({ const field = await prisma.field.upsert({

View File

@ -1,8 +1,4 @@
import { import { defaultHandler, defaultResponder } from "@documenso/lib/server";
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma"; import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { SigningStatus, DocumentStatus } from "@prisma/client"; import { SigningStatus, DocumentStatus } from "@prisma/client";
@ -12,7 +8,6 @@ import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
import { sendSigningDoneMail } from "@documenso/lib/mail"; import { sendSigningDoneMail } from "@documenso/lib/mail";
async function postHandler(req: NextApiRequest, res: NextApiResponse) { async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const existingUser = await getUserFromToken(req, res);
const { token: recipientToken } = req.query; const { token: recipientToken } = req.query;
const { signatures: signaturesFromBody }: { signatures: any[] } = req.body; const { signatures: signaturesFromBody }: { signatures: any[] } = req.body;
@ -29,11 +24,19 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
return res.status(401).send("Recipient not found."); return res.status(401).send("Recipient not found.");
} }
const document: PrismaDocument = await getDocument( const document: PrismaDocument = await prisma.document.findFirstOrThrow({
recipient.documentId, where: {
req, id: recipient.documentId,
res },
); include: {
Recipient: {
orderBy: {
id: "asc",
},
},
Field: { include: { Recipient: true, Signature: true } },
},
});
if (!document) res.status(404).end(`No document found.`); if (!document) res.status(404).end(`No document found.`);
@ -70,6 +73,8 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
}, },
}); });
// 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({ const nonSignatureFields = await prisma.field.findMany({
where: { where: {
documentId: document.id, documentId: document.id,

View File

@ -107,7 +107,6 @@ export async function getServerSideProps(context: any) {
where: { where: {
documentId: recipient.Document.id, documentId: recipient.Document.id,
recipientId: recipient.id, recipientId: recipient.id,
type: { in: [FieldType.SIGNATURE] },
Signature: { is: null }, Signature: { is: null },
}, },
include: { include: {

View File

@ -6,7 +6,7 @@ import { Button, IconButton } from "@documenso/ui";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
const SignPage: NextPageWithLayout = (props: any) => { const Signed: NextPageWithLayout = (props: any) => {
const router = useRouter(); const router = useRouter();
const allRecipientsSigned = props.document.Recipient?.every( const allRecipientsSigned = props.document.Recipient?.every(
(r: any) => r.signingStatus === "SIGNED" (r: any) => r.signingStatus === "SIGNED"
@ -47,7 +47,12 @@ const SignPage: NextPageWithLayout = (props: any) => {
onClick={(event: any) => { onClick={(event: any) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
router.push("/api/documents/" + props.document.id); router.push(
"/api/documents/" +
props.document.id +
"?token=" +
props.recipient.token
);
}} }}
> >
Download "{props.document.title}" Download "{props.document.title}"
@ -103,8 +108,9 @@ export async function getServerSideProps(context: any) {
props: { props: {
document: JSON.parse(JSON.stringify(recipient.Document)), document: JSON.parse(JSON.stringify(recipient.Document)),
fields: JSON.parse(JSON.stringify(fields)), fields: JSON.parse(JSON.stringify(fields)),
recipient: JSON.parse(JSON.stringify(recipient)),
}, },
}; };
} }
export default SignPage; export default Signed;

View File

@ -1,13 +1,23 @@
import Head from "next/head"; import Head from "next/head";
import Login from "../components/login"; import Login from "../components/login";
export default function LoginPage() { export default function LoginPage(props: any) {
return ( return (
<> <>
<Head> <Head>
<title>Login | Documenso</title> <title>Login | Documenso</title>
</Head> </Head>
<Login></Login> <Login allowSignup={props.ALLOW_SIGNUP}></Login>
</> </>
); );
} }
export async function getServerSideProps(context: any) {
const ALLOW_SIGNUP = process.env.ALLOW_SIGNUP === "true";
return {
props: {
ALLOW_SIGNUP: ALLOW_SIGNUP,
},
};
}

View File

@ -14,6 +14,14 @@ export default function SignupPage(props: { source: string }) {
} }
export async function getServerSideProps(context: any) { export async function getServerSideProps(context: any) {
if (process.env.ALLOW_SIGNUP !== "true")
return {
redirect: {
destination: "/login",
permanent: false,
},
};
const signupSource: string = context.query["source"]; const signupSource: string = context.query["source"];
return { return {
props: { props: {

13
package-lock.json generated
View File

@ -40,6 +40,7 @@
} }
}, },
"apps/web": { "apps/web": {
"name": "@documenso/web",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@documenso/pdf": "*", "@documenso/pdf": "*",
@ -3555,7 +3556,8 @@
"node_modules/json-parse-even-better-errors": { "node_modules/json-parse-even-better-errors": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"peer": true
}, },
"node_modules/json-schema": { "node_modules/json-schema": {
"version": "0.4.0", "version": "0.4.0",
@ -8639,18 +8641,22 @@
} }
}, },
"packages/features": { "packages/features": {
"name": "@documenso/features",
"version": "0.0.0" "version": "0.0.0"
}, },
"packages/lib": { "packages/lib": {
"name": "@documenso/lib",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"bcryptjs": "^2.4.3" "bcryptjs": "^2.4.3"
} }
}, },
"packages/pdf": { "packages/pdf": {
"name": "@documenso/pdf",
"version": "0.0.0" "version": "0.0.0"
}, },
"packages/prisma": { "packages/prisma": {
"name": "@documenso/prisma",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@prisma/client": "^4.8.1", "@prisma/client": "^4.8.1",
@ -8669,9 +8675,11 @@
"dev": true "dev": true
}, },
"packages/signing": { "packages/signing": {
"name": "@documenso/signing",
"version": "0.0.0" "version": "0.0.0"
}, },
"packages/ui": { "packages/ui": {
"name": "@documenso/ui",
"version": "0.0.0", "version": "0.0.0",
"devDependencies": {} "devDependencies": {}
} }
@ -11283,7 +11291,8 @@
"json-parse-even-better-errors": { "json-parse-even-better-errors": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"peer": true
}, },
"json-schema": { "json-schema": {
"version": "0.4.0", "version": "0.4.0",

View File

@ -2,11 +2,12 @@ import toast from "react-hot-toast";
export const createOrUpdateField = async ( export const createOrUpdateField = async (
document: any, document: any,
field: any field: any,
recipientToken: string = ""
): Promise<any> => { ): Promise<any> => {
try { try {
const created = await toast.promise( const created = await toast.promise(
fetch("/api/documents/" + document.id + "/fields", { fetch("/api/documents/" + document.id + "/fields?token=" + recipientToken, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@ -12,7 +12,7 @@ export async function getUserFromToken(
const tokenEmail = token?.email?.toString(); const tokenEmail = token?.email?.toString();
if (!token) { if (!token) {
res.status(401).send("No session token found for request."); if (res.status) res.status(401).send("No session token found for request.");
return null; return null;
} }