Compare commits

...

41 Commits

Author SHA1 Message Date
Mythie
3eebd52dd1 fix: improve types 2023-04-19 23:52:37 +10:00
Saurav Gurhale
f8f4c07d11 add toast error for invalid email 2023-04-19 23:48:21 +10:00
Lucas Smith
52f554a636 Merge pull request #136 from dephraiim/doc-223
Remove Input Placeholders
2023-04-19 22:55:26 +10:00
Ephraim Atta-Duncan
bcc2530484 Declutter Textarea by removing placeholders 2023-04-16 23:45:57 +00:00
Timur Ercan
5c58b32d92 Merge branch 'main' of https://github.com/documenso/documenso 2023-04-15 20:35:36 +02:00
Timur Ercan
f10bafd998 cleanup 2023-04-15 20:35:33 +02:00
Timur Ercan
2cf8896e46 Merge branch 'main' of https://github.com/documenso/documenso 2023-04-15 20:33:38 +02:00
Timur Ercan
e873af3ec9 cleanup 2023-04-15 20:31:38 +02:00
Timur Ercan
06501bde60 cleanup 2023-04-15 20:31:24 +02:00
Timur Ercan
0dcab27e65 fix: openshift build does not allow private repos 2023-04-15 20:26:35 +02:00
Timur Ercan
ff2334ab55 fix: openshift build does not allow private repos
https://github.com/documenso/documenso/issues/79
2023-04-15 20:18:30 +02:00
Timur Ercan
63bd044723 feat: npm run d for ultra quick start 2023-04-15 20:04:28 +02:00
Timur Ercan
b111874d7c fix: redirect users sessions not found in databae 2023-04-15 19:54:04 +02:00
Timur Ercan
21149f82ba Merge pull request #61 from Mythie/feat/docker-environment
feat: add docker support and docker-compose quickstart
2023-04-15 19:44:33 +02:00
Mythie
cb77a40fd9 fix: update postgres port 2023-04-13 23:43:42 +10:00
Mythie
7aa7485388 fix: migrate dx.sh to package scripts 2023-04-13 22:52:54 +10:00
Timur Ercan
984084dd3b Merge branch 'main' into feat/docker-environment 2023-04-13 14:50:36 +02:00
Timur Ercan
421327432a added migration for doc-208 (allow document delete with sigantures) 2023-04-11 16:19:18 +02:00
Timur Ercan
134e366c27 Merge pull request #45 from SauravL3010/fix-#41-db-migration-Signature_recipientId_fkey
Fix-#41: Change referential action for Signature_recipientId_fkey
2023-04-11 15:50:22 +02:00
Timur Ercan
c79592cd0a Merge branch 'main' into fix-#41-db-migration-Signature_recipientId_fkey 2023-04-11 15:34:32 +02:00
Timur Ercan
f7cc44f138 Merge pull request #63 from dephraiim/doc-205
Disable the edit and add signer button for completed documents
2023-04-11 15:33:25 +02:00
Timur Ercan
60ff4fc992 Merge pull request #64 from dephraiim/doc-213
Send email notification to signers on document signing completion
2023-04-11 15:12:54 +02:00
Ephraim Atta-Duncan
e4e44b7f22 Replace fragment with null 2023-04-10 01:34:20 +00:00
Ephraim Atta-Duncan
6034e7a21e Send email notification to signers on document signing completion with signed document 2023-04-09 13:15:44 +00:00
Ephraim Atta-Duncan
2a34cc26c6 Replace empty string with fragments 2023-04-09 12:39:18 +00:00
Mythie
6ea38efd9d chore: tidy script 2023-04-09 22:36:28 +10:00
Ephraim Atta-Duncan
0ce66a7957 Redirect breadcrump link on completed to avoid editing 2023-04-09 12:34:26 +00:00
Mythie
49cb50ed6e feat: add down flag for stopping environment 2023-04-09 22:33:14 +10:00
Ephraim Atta-Duncan
065efabb39 Change wording on completed signers page 2023-04-09 12:29:31 +00:00
Ephraim Atta-Duncan
e86d4cc719 Disable the edit and add signer button for completed documents 2023-04-09 12:26:48 +00:00
Mythie
5dd3713475 feat: add docker support and docker-compose quickstart
Add support for production container builds using the provided `Dockerfile` and `build.sh` script. This can later be used with actions to automatically publish to the provided docker registry.

Additionally, support an accelerated developer quickstart using `docker-compose`. Developers can now run the `dx` npm command to quickly spin up a database and mail server.
2023-04-08 23:20:42 +10:00
Timur Ercan
30c1c76dd7 Merge pull request #44 from SauravL3010/fix-recipient-selector
small fix for recipient-selector
2023-04-07 10:52:49 +02:00
Timur Ercan
22e191e98c Merge pull request #38 from Mythie/fix/improve-text-insertion-accuracy
fix: improve text insertion accuracy
2023-04-07 10:46:23 +02:00
Saurav Gurhale
5db54d3b8c Cange referential action for Signature_recipientId_fkey 2023-04-06 21:26:26 -04:00
Saurav Gurhale
593c317bf1 small fix for recipient-selector
ListBox options must be unique
2023-04-06 14:09:08 -04:00
Mythie
ee4ca018d8 fix: improve text insertion accuracy
Previous inserted text would appear a little off center from where the user had selected which could cause some frustration.

We improve upon this by updating the code responsible for centering the text to behave in a more accurate manner. From what I can tell it looks to be quite solid but could do with more rigorous testing with shorter and longer inputs.

You can see the improved accuracy in action here:

https://www.loom.com/share/1095fee7605c4790b8b30f573a04f0f0
2023-04-06 23:34:53 +10:00
Timur Ercan
e3db462587 Merge pull request #34 from dephraiim/prettier-config
[new] Add `prettier`
2023-04-06 15:03:39 +02:00
Timur Ercan
739d29d753 Merge branch 'main' into prettier-config 2023-04-06 15:02:17 +02:00
Ephraim Atta-Duncan
964e749039 Update prettier styling 2023-04-04 22:10:30 +00:00
Ephraim Atta-Duncan
84b57d715c Apply prettier config to all files 2023-04-04 22:02:32 +00:00
Ephraim Atta-Duncan
85f2b5e84a Add prettier config 2023-04-04 22:00:01 +00:00
109 changed files with 2854 additions and 1374 deletions

38
.dockerignore Normal file
View File

@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
**/node_modules
**/.pnp
**.pnp.js
# testing
**/coverage
# next.js
**/.next/
**/out/
# production
**/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env
.env.example

View File

@@ -1,6 +1,9 @@
# Database
# Option 1: You can use the provided remote test database, courtesy of the documenso team: postgres://documenso_test_user:GnmLG14u12sd9zHsd4vVWwP40WneFJMo@dpg-cf2hljh4reb5o45oqpq0-a.oregon-postgres.render.com/documenso_test_e2i3
# Option 2: Set up a local Postgres SQL instance (RECOMMENDED)
# Option 3: Use the provided dx setup (RECOMMENDED)
# => postgres://documenso:password@127.0.0.1:54320/documenso
#
# ⚠ WARNING: The test database can be resetted or taken offline at any point.
# ⚠ WARNING: Please be aware that nothing written to the test databae is private.
DATABASE_URL=''
@@ -20,6 +23,12 @@ SENDGRID_API_KEY=''
# SMTP
# Set SMTP credentials to use SMTP instead of the Sendgrid API.
# If you're using the dx setup you can use the following values:
#
# SMTP_MAIL_HOST='127.0.0.1'
# SMTP_MAIL_PORT='2500'
# SMTP_MAIL_USER='documenso'
# SMTP_MAIL_PASSWORD='documenso'
SMTP_MAIL_HOST=''
SMTP_MAIL_PORT=''
SMTP_MAIL_USER=''
@@ -30,4 +39,4 @@ MAIL_FROM='documenso@localhost.com'
#FEATURE FLAGS
# Allow users to register via the /signup page. Otherwise they will be redirect to the home page.
ALLOW_SIGNUP=true
ALLOW_SIGNUP=true

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "apps/website/documenso/website"]
path = apps/website/documenso/website
url = http://github.com/documenso/website.git

View File

@@ -99,6 +99,33 @@ To run Documenso locally you need
- Node Package Manger NPM - included in Node.js
- [PostgreSQL (local or remote)](https://www.postgresql.org/download/)
## Developer Quickstart
> **Note**: This is a quickstart for developers. It assumes that you have both [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/) installed on your machine.
Want to get up and running quickly? Follow these steps:
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh
git clone https://github.com/documenso/documenso
```
- Set up your .env file using the recommendations in the .env.example file.
- Run `npm run dx` in the root directory
- This will spin up a postgres database and inbucket mail server in docker containers.
- Run `npm run dev` in the root directory
- Want it even faster? Just use
```sh
npm run d
```
That's it! You should now be able to access the app at http://localhost:3000
Incoming mail will be available at http://localhost:9000
Your database will also be available on port `5432`. You can connect to it using your favorite database client.
## Developer Setup
Follow these steps to setup documenso on you local machnine:
@@ -121,6 +148,7 @@ Follow these steps to setup documenso on you local machnine:
- Register a new user at http://localhost:3000/signup
---
- Optional: Seed the database using <code>npm run db-seed</code> to create a test user and document
- Optional: Upload and sign <code>apps\web\ressources\example.pdf</code> manually to test your setup
@@ -152,6 +180,15 @@ For the digital signature of you documents you need a signign certificate in .p1
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
5. Place the certificate <code>/apps/web/ressource/certificate.p12</code>
# Docker
> We are still working on the publishing of docker images, in the meantime you can follow the steps below to create a production ready docker image.
Want to create a production ready docker image? Follow these steps:
- Run `./docker/build.sh` in the root directory.
- Publish the image to your docker registry of choice.
# Deploying - Coming Soon™
- Docker support

View File

@@ -1,8 +1,9 @@
import React, { useState } from "react";
import Draggable from "react-draggable";
import Logo from "../logo";
import { IconButton } from "@documenso/ui";
import Logo from "../logo";
import { XCircleIcon } from "@heroicons/react/20/solid";
import Draggable from "react-draggable";
const stc = require("string-to-color");
type FieldPropsType = {
@@ -51,21 +52,19 @@ export default function EditableField(props: FieldPropsType) {
onMouseDown={(e: any) => {
e.preventDefault();
e.stopPropagation();
}}
>
}}>
{/* width: 192 height 96 */}
<div
hidden={props.hidden}
ref={nodeRef}
className="cursor-move opacity-80 p-2 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none"
className="absolute top-0 left-0 m-auto h-16 w-48 cursor-move select-none flex-row-reverse p-2 text-center text-lg font-bold opacity-80"
style={{
background: stc(props.field.Recipient.email),
}}
>
<div className="m-auto overflow-hidden flex-row-reverse text-lg font-bold text-center">
}}>
<div className="m-auto flex-row-reverse overflow-hidden text-center text-lg font-bold">
{field.type}
{field.type === "SIGNATURE" ? (
<div className="text-xs text-center">
<div className="text-center text-xs">
{`${props.field.Recipient?.name} <${props.field.Recipient?.email}>`}
</div>
) : (
@@ -79,8 +78,7 @@ export default function EditableField(props: FieldPropsType) {
icon={XCircleIcon}
onClick={(event: any) => {
props.onDelete(props.field.id);
}}
></IconButton>
}}></IconButton>
</strong>
</div>
</Draggable>

View File

@@ -1,7 +1,8 @@
import { useEffect, useState } from "react";
import { RadioGroup } from "@headlessui/react";
import { classNames } from "@documenso/lib";
import { RadioGroup } from "@headlessui/react";
import { FieldType } from "@prisma/client";
const stc = require("string-to-color");
export default function FieldTypeSelector(props: any) {
@@ -24,8 +25,7 @@ export default function FieldTypeSelector(props: any) {
value={selectedFieldType}
onChange={(e: any) => {
setSelectedFieldType(e);
}}
>
}}>
<div className="space-y-4">
{fieldTypes.map((fieldType) => (
<RadioGroup.Option
@@ -37,30 +37,23 @@ export default function FieldTypeSelector(props: any) {
className={({ checked, active }) =>
classNames(
checked ? "border-neon border-2" : "border-transparent",
"hover:bg-slate-100 select-none relative block cursor-pointer rounded-lg border bg-white px-3 py-2 focus:outline-none sm:flex sm:justify-between"
"relative block cursor-pointer select-none rounded-lg border bg-white px-3 py-2 hover:bg-slate-100 focus:outline-none sm:flex sm:justify-between"
)
}
>
}>
{({ active, checked }) => (
<>
<span className="flex items-center">
<span className="flex flex-col text-sm">
<RadioGroup.Label
as="span"
className="font-medium text-gray-900"
>
<RadioGroup.Label as="span" className="font-medium text-gray-900">
<span
className="inline-block h-4 w-4 flex-shrink-0 rounded-full mr-3 align-middle"
className="mr-3 inline-block h-4 w-4 flex-shrink-0 rounded-full align-middle"
style={{
background: stc(props.selectedRecipient?.email),
}}
/>
<span className="align-middle">
{" "}
{
fieldTypes.filter((e) => e.id === fieldType.id)[0]
.name
}
{fieldTypes.filter((e) => e.id === fieldType.id)[0].name}
</span>
</RadioGroup.Label>
</span>

View File

@@ -1,13 +1,14 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
import { useState } from "react";
import { createOrUpdateField, deleteField } from "@documenso/lib/api";
import { createField } from "@documenso/features/editor";
import RecipientSelector from "./recipient-selector";
import FieldTypeSelector from "./field-type-selector";
import { InformationCircleIcon } from "@heroicons/react/24/outline";
import dynamic from "next/dynamic";
import Link from "next/link";
import { useRouter } from "next/router";
import { createField } from "@documenso/features/editor";
import { createOrUpdateField, deleteField } from "@documenso/lib/api";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import FieldTypeSelector from "./field-type-selector";
import RecipientSelector from "./recipient-selector";
import { InformationCircleIcon } from "@heroicons/react/24/outline";
const stc = require("string-to-color");
const PDFViewer = dynamic(() => import("./pdf-viewer"), {
@@ -20,8 +21,7 @@ export default function PDFEditor(props: any) {
const [selectedRecipient, setSelectedRecipient]: any = useState();
const [selectedFieldType, setSelectedFieldType] = useState();
const noRecipients =
props?.document.Recipient.length === 0 ||
props?.document.Recipient.every((e: any) => !e.email);
props?.document.Recipient.length === 0 || props?.document.Recipient.every((e: any) => !e.email);
function onPositionChangedHandler(position: any, id: any) {
if (!position) return;
@@ -53,26 +53,16 @@ export default function PDFEditor(props: any) {
<div hidden={!noRecipients} className="rounded-md bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-yellow-400"
aria-hidden="true"
/>
<InformationCircleIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-yellow-700">
This document does not have any recipients. Add recipients to
create fields.
This document does not have any recipients. Add recipients to create fields.
</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
<Link
href={
NEXT_PUBLIC_WEBAPP_URL +
"/documents/" +
props.document.id +
"/recipients"
}
className="whitespace-nowrap font-medium text-yellow-700 hover:text-yellow-600"
>
href={NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients"}
className="whitespace-nowrap font-medium text-yellow-700 hover:text-yellow-600">
Add Recipients
<span aria-hidden="true"> &rarr;</span>
</Link>
@@ -98,12 +88,10 @@ export default function PDFEditor(props: any) {
}}
onMouseDown={(e: any, page: number) => {
if (e.button === 0) addField(e, page);
}}
></PDFViewer>
}}></PDFViewer>
<div
hidden={noRecipients}
className="fixed left-0 top-1/3 max-w-xs border border-slate-300 bg-white py-4 pr-5 rounded-md"
>
className="fixed left-0 top-1/3 max-w-xs rounded-md border border-slate-300 bg-white py-4 pr-5">
<RecipientSelector
recipients={props?.document?.Recipient}
onChange={setSelectedRecipient}
@@ -123,12 +111,7 @@ export default function PDFEditor(props: any) {
if (!selectedFieldType) return;
if (noRecipients) return;
const signatureField = createField(
e,
page,
selectedRecipient,
selectedFieldType
);
const signatureField = createField(e, page, selectedRecipient, selectedFieldType);
createOrUpdateField(props?.document, signatureField).then((res) => {
setFields((prevState) => [...prevState, res]);

View File

@@ -1,21 +1,14 @@
import Logo from "../logo";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
import SignatureDialog from "./signature-dialog";
import { useEffect, useState } from "react";
import { Button } from "@documenso/ui";
import {
CheckBadgeIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
import { FieldType } from "@prisma/client";
import {
createOrUpdateField,
deleteField,
signDocument,
} from "@documenso/lib/api";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { createField } from "@documenso/features/editor";
import { createOrUpdateField, deleteField, signDocument } from "@documenso/lib/api";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { Button } from "@documenso/ui";
import Logo from "../logo";
import SignatureDialog from "./signature-dialog";
import { CheckBadgeIcon, InformationCircleIcon } from "@heroicons/react/24/outline";
import { FieldType } from "@prisma/client";
const PDFViewer = dynamic(() => import("./pdf-viewer"), {
ssr: false,
@@ -27,9 +20,7 @@ export default function PDFSigner(props: any) {
const [signingDone, setSigningDone] = useState(false);
const [localSignatures, setLocalSignatures] = useState<any[]>([]);
const [fields, setFields] = useState<any[]>(props.fields);
const signatureFields = fields.filter(
(field) => field.type === FieldType.SIGNATURE
);
const signatureFields = fields.filter((field) => field.type === FieldType.SIGNATURE);
const [dialogField, setDialogField] = useState<any>();
useEffect(() => {
@@ -80,9 +71,9 @@ export default function PDFSigner(props: any) {
<div className="bg-neon p-4">
<div className="flex">
<div className="flex-shrink-0">
<Logo className="h-12 w-12 -mt-2.5"></Logo>
<Logo className="-mt-2.5 h-12 w-12"></Logo>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between text-center justify-start items-center">
<div className="ml-3 flex-1 items-center justify-start text-center md:flex md:justify-between">
<p className="text-lg text-slate-700">
{props.document.User.name
? `${props.document.User.name} (${props.document.User.email})`
@@ -96,17 +87,14 @@ export default function PDFSigner(props: any) {
icon={CheckBadgeIcon}
className="float-right"
onClick={() => {
signDocument(
props.document,
localSignatures,
`${router.query.token}`
).then(() => {
router.push(
`/documents/${props.document.id}/signed?token=${router.query.token}`
);
});
}}
>
signDocument(props.document, localSignatures, `${router.query.token}`).then(
() => {
router.push(
`/documents/${props.document.id}/signed?token=${router.query.token}`
);
}
);
}}>
Done
</Button>
</p>
@@ -117,15 +105,11 @@ export default function PDFSigner(props: any) {
<div className="bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-yellow-400"
aria-hidden="true"
/>
<InformationCircleIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-yellow-700">
You can sign this document anywhere you like, but maybe look for
a signature line.
You can sign this document anywhere you like, but maybe look for a signature line.
</p>
</div>
</div>
@@ -144,12 +128,10 @@ export default function PDFSigner(props: any) {
pdfUrl={`${NEXT_PUBLIC_WEBAPP_URL}/api/documents/${router.query.id}?token=${router.query.token}`}
onClick={onClick}
onMouseDown={function onMouseDown(e: any, page: number) {
if (signatureFields.length === 0)
addFreeSignature(e, page, props.recipient);
if (signatureFields.length === 0) addFreeSignature(e, page, props.recipient);
}}
onMouseUp={() => {}}
onDelete={onDeleteHandler}
></PDFViewer>
onDelete={onDeleteHandler}></PDFViewer>
</>
);
@@ -166,18 +148,9 @@ export default function PDFSigner(props: any) {
}
function addFreeSignature(e: any, page: number, recipient: any): any {
const freeSignatureField = createField(
e,
page,
recipient,
FieldType.FREE_SIGNATURE
);
const freeSignatureField = createField(e, page, recipient, FieldType.FREE_SIGNATURE);
createOrUpdateField(
props.document,
freeSignatureField,
recipient.token
).then((res) => {
createOrUpdateField(props.document, freeSignatureField, recipient.token).then((res) => {
setFields((prevState) => [...prevState, res]);
setDialogField(res);
setOpen(true);

View File

@@ -1,9 +1,9 @@
import { Fragment, useState } from "react";
import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
import EditableField from "./editable-field";
import SignableField from "./signable-field";
import short from "short-uuid";
import { FieldType } from "@prisma/client";
import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
import short from "short-uuid";
export default function PDFViewer(props) {
const [numPages, setNumPages] = useState(null);
@@ -33,16 +33,14 @@ export default function PDFViewer(props) {
<div
hidden={loading}
onMouseUp={props.onMouseUp}
style={{ height: numPages * pageHeight + 1000 }}
>
<div className="max-w-xs mt-6"></div>
style={{ height: numPages * pageHeight + 1000 }}>
<div className="mt-6 max-w-xs"></div>
<Document
file={props.pdfUrl}
onLoadSuccess={onDocumentLoadSuccess}
options={options}
renderMode="canvas"
className="absolute w-auto mx-auto left-0 right-0"
>
className="absolute left-0 right-0 mx-auto w-auto">
{Array.from({ length: numPages }, (_, index) => (
<Fragment key={short.generate().toString()}>
<div
@@ -57,8 +55,7 @@ export default function PDFViewer(props) {
position: "relative",
...props.style,
}}
className="mx-auto w-fit"
>
className="mx-auto w-fit">
<Page
className="mt-5"
key={`page_${index + 1}`}
@@ -69,8 +66,7 @@ export default function PDFViewer(props) {
if (e.height) setPageHeight(e.height);
setLoading(false);
}}
onRenderError={() => setLoading(false)}
></Page>
onRenderError={() => setLoading(false)}></Page>
{props?.fields
.filter((field) => field.page === index)
.map((field) =>
@@ -80,8 +76,7 @@ export default function PDFViewer(props) {
key={field.id}
field={field}
className="absolute"
onDelete={onDeleteHandler}
></SignableField>
onDelete={onDeleteHandler}></SignableField>
) : (
<EditableField
hidden={
@@ -93,8 +88,7 @@ export default function PDFViewer(props) {
field={field}
className="absolute"
onPositionChanged={onPositionChangedHandler}
onDelete={onDeleteHandler}
></EditableField>
onDelete={onDeleteHandler}></EditableField>
)
)}
</div>

View File

@@ -1,13 +1,12 @@
import { Fragment, useEffect, useState } from "react";
import { classNames } from "@documenso/lib";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline";
import { classNames } from "@documenso/lib";
const stc = require("string-to-color");
export default function RecipientSelector(props: any) {
const [selectedRecipient, setSelectedRecipient]: any = useState(
props?.recipients[0]
);
const [selectedRecipient, setSelectedRecipient]: any = useState(props?.recipients[0]);
useEffect(() => {
props.onChange(selectedRecipient);
@@ -18,11 +17,10 @@ export default function RecipientSelector(props: any) {
value={selectedRecipient}
onChange={(e: any) => {
setSelectedRecipient(e);
}}
>
}}>
{({ open }) => (
<div className="relative mt-1 mb-2">
<Listbox.Button className="select-none relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left focus:border-neon focus:outline-none focus:ring-1 focus:ring-neon sm:text-sm">
<Listbox.Button className="focus:border-neon focus:ring-neon relative w-full cursor-default select-none rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-1 sm:text-sm">
<span className="flex items-center">
<span
className="inline-block h-4 w-4 flex-shrink-0 rounded-full"
@@ -33,10 +31,7 @@ export default function RecipientSelector(props: any) {
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>
@@ -45,20 +40,18 @@ export default function RecipientSelector(props: any) {
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
leaveTo="opacity-0">
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{props?.recipients.map((recipient: any) => (
<Listbox.Option
key={recipient?.id}
className={({ active }) =>
classNames(
active ? "text-white bg-neon-dark" : "text-gray-900",
active ? "bg-neon-dark text-white" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9"
)
}
value={recipient}
>
value={recipient}>
{({ selected, active }) => (
<>
<div className="flex items-center">
@@ -72,9 +65,8 @@ export default function RecipientSelector(props: any) {
className={classNames(
selected ? "font-semibold" : "font-normal",
"ml-3 block truncate"
)}
>
{`${selectedRecipient?.name} <${selectedRecipient?.email}>`}
)}>
{`${recipient?.name} <${recipient?.email}>`}
</span>
</div>
@@ -83,8 +75,7 @@ export default function RecipientSelector(props: any) {
className={classNames(
active ? "text-white" : "text-neon-dark",
"absolute inset-y-0 right-0 flex items-center pr-4"
)}
>
)}>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}

View File

@@ -1,8 +1,9 @@
import React, { useState } from "react";
import Draggable from "react-draggable";
import { classNames } from "@documenso/lib";
import { IconButton } from "@documenso/ui";
import { XCircleIcon } from "@heroicons/react/20/solid";
import { classNames } from "@documenso/lib";
import Draggable from "react-draggable";
const stc = require("string-to-color");
type FieldPropsType = {
@@ -37,31 +38,26 @@ export default function SignableField(props: FieldPropsType) {
onMouseDown={(e: any) => {
// e.preventDefault();
e.stopPropagation();
}}
>
}}>
<div
onClick={(e: any) => {
if (!field?.signature) props.onClick(props.field);
}}
ref={nodeRef}
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"
"absolute top-0 left-0 m-auto h-16 w-48 select-none flex-row-reverse text-center text-lg font-bold opacity-80",
field.type === "SIGNATURE" ? "cursor-pointer hover:brightness-50" : "cursor-not-allowed"
)}
style={{
background: stc(props.field.Recipient.email),
}}
>
<div hidden={field?.signature} className="font-medium my-4">
}}>
<div hidden={field?.signature} className="my-4 font-medium">
{field.type === "SIGNATURE" ? "SIGN HERE" : ""}
{field.type === "DATE" ? <small>Date (filled on sign)</small> : ""}
</div>
<div
hidden={!field?.signature}
className="font-qwigley text-5xl m-auto w-auto flex-row-reverse font-medium text-center"
>
className="font-qwigley m-auto w-auto flex-row-reverse text-center text-5xl font-medium">
{field?.signature?.type === "type" ? (
<div className="my-4">{field?.signature.typedSignature}</div>
) : (
@@ -69,7 +65,7 @@ export default function SignableField(props: FieldPropsType) {
)}
{field?.signature?.type === "draw" ? (
<img className="w-48 h-16" src={field?.signature?.signatureImage} />
<img className="h-16 w-48" src={field?.signature?.signatureImage} />
) : (
""
)}

View File

@@ -1,14 +1,10 @@
import { Fragment, useEffect, useState } from "react";
import { classNames } from "@documenso/lib";
import { localStorage } from "@documenso/lib";
import { Button, IconButton } from "@documenso/ui";
import { Dialog, Transition } from "@headlessui/react";
import {
LanguageIcon,
PencilIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { Fragment, useEffect, useState } from "react";
import { LanguageIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
import SignatureCanvas from "react-signature-canvas";
import { localStorage } from "@documenso/lib";
const tabs = [
{ name: "Type", icon: LanguageIcon, current: true },
@@ -34,8 +30,7 @@ export default function SignatureDialog(props: any) {
onClose={() => {
props.setOpen(false);
setCurrent(tabs[0]);
}}
>
}}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@@ -43,8 +38,7 @@ export default function SignatureDialog(props: any) {
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
leaveTo="opacity-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
@@ -57,11 +51,10 @@ export default function SignatureDialog(props: any) {
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="min-h-[350px] relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<Dialog.Panel className="relative min-h-[350px] transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
<div className="">
<div className="border-b border-gray-200 mb-3">
<div className="mb-3 border-b border-gray-200">
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<a
@@ -72,11 +65,10 @@ export default function SignatureDialog(props: any) {
className={classNames(
tab.current
? "border-neon text-neon"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
"group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm cursor-pointer"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"group inline-flex cursor-pointer items-center border-b-2 py-4 px-1 text-sm font-medium"
)}
aria-current={tab.current ? "page" : undefined}
>
aria-current={tab.current ? "page" : undefined}>
<tab.icon
className={classNames(
tab.current
@@ -93,7 +85,7 @@ export default function SignatureDialog(props: any) {
</div>
{isCurrentTab("Type") ? (
<div>
<div className="my-8 border-b border-gray-300 mb-3">
<div className="my-8 mb-3 border-b border-gray-300">
<input
value={typedSignature}
onChange={(e) => {
@@ -101,7 +93,7 @@ export default function SignatureDialog(props: any) {
}}
className={classNames(
typedSignature ? "font-qwigley text-4xl" : "",
"leading-none h-10 align-bottom mt-14 text-center block w-full focus:border-neon focus:ring-neon text-2xl"
"focus:border-neon focus:ring-neon mt-14 block h-10 w-full text-center align-bottom text-2xl leading-none"
)}
placeholder="Kindly type your name"
/>
@@ -113,24 +105,19 @@ export default function SignatureDialog(props: any) {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
}}
>
}}>
Cancel
</Button>
<Button
className="ml-3"
disabled={!typedSignature}
onClick={() => {
localStorage.setItem(
"typedSignature",
typedSignature
);
localStorage.setItem("typedSignature", typedSignature);
props.onClose({
type: "type",
typedSignature: typedSignature,
});
}}
>
}}>
Sign
</Button>
</div>
@@ -145,8 +132,7 @@ export default function SignatureDialog(props: any) {
signCanvasRef = ref;
}}
canvasProps={{
className:
"sigCanvas border-b b-2 border-slate w-full h-full mb-3",
className: "sigCanvas border-b b-2 border-slate w-full h-full mb-3",
}}
clearOnResize={true}
onEnd={() => {
@@ -154,22 +140,20 @@ export default function SignatureDialog(props: any) {
}}
/>
<IconButton
className="block float-left"
className="float-left block"
icon={TrashIcon}
onClick={() => {
signCanvasRef?.clear();
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
></IconButton>
<div className="mt-10 float-right">
}}></IconButton>
<div className="float-right mt-10">
<Button
color="secondary"
onClick={() => {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
}}
>
}}>
Cancel
</Button>
<Button
@@ -177,12 +161,10 @@ export default function SignatureDialog(props: any) {
onClick={() => {
props.onClose({
type: "draw",
signatureImage:
signCanvasRef.toDataURL("image/png"),
signatureImage: signCanvasRef.toDataURL("image/png"),
});
}}
disabled={signatureEmpty}
>
disabled={signatureEmpty}>
Sign
</Button>
</div>
@@ -200,11 +182,11 @@ export default function SignatureDialog(props: any) {
</>
);
function isCurrentTab(tabName: string): boolean {
function isCurrentTab(tabName: string): boolean {
return currentTab.name === tabName;
}
function setCurrent(t: any) {
function setCurrent(t: any) {
tabs.forEach((tab) => {
tab.current = tab.name === t.name;
});

View File

@@ -1,9 +1,8 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import Navigation from "./navigation";
import { useSession } from "next-auth/react";
function useRedirectToLoginIfUnauthenticated() {
const { data: session, status } = useSession();

View File

@@ -1,14 +1,13 @@
import { LockClosedIcon } from "@heroicons/react/20/solid";
import Link from "next/link";
import { FormProvider, useForm } from "react-hook-form";
import Logo from "./logo";
import { signIn } from "next-auth/react";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { toast } from "react-hot-toast";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { Button } from "@documenso/ui";
import Logo from "./logo";
import { LockClosedIcon } from "@heroicons/react/20/solid";
import { signIn } from "next-auth/react";
import { FormProvider, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
interface LoginValues {
email: string;
@@ -22,10 +21,7 @@ export default function Login(props: any) {
const methods = useForm<LoginValues>();
const { register, formState } = methods;
const [errorMessage, setErrorMessage] = useState<string | null>(null);
let callbackUrl =
typeof router.query?.callbackUrl === "string"
? router.query.callbackUrl
: "";
let callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : "";
// If not absolute URL, make it absolute
if (!/^https?:\/\//.test(callbackUrl)) {
@@ -79,10 +75,7 @@ export default function Login(props: any) {
</h2>
</div>
<FormProvider {...methods}>
<form
className="mt-8 space-y-6"
onSubmit={methods.handleSubmit(onSubmit)}
>
<form className="mt-8 space-y-6" onSubmit={methods.handleSubmit(onSubmit)}>
<input type="hidden" name="remember" defaultValue="true" />
<div className="-space-y-px rounded-md shadow-sm">
<div>
@@ -96,7 +89,7 @@ export default function Login(props: any) {
type="email"
autoComplete="email"
required
className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="Email"
/>
</div>
@@ -111,14 +104,14 @@ export default function Login(props: any) {
type="password"
autoComplete="current-password"
required
className="relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="Password"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="text-sm">
<a href="#" className="font-medium text-neon hover:text-neon">
<a href="#" className="text-neon hover:text-neon font-medium">
Forgot your password?
</a>
</div>
@@ -127,11 +120,10 @@ export default function Login(props: any) {
<Button
type="submit"
disabled={formState.isSubmitting}
className="group relative flex w-full"
>
className="group relative flex w-full">
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
<LockClosedIcon
className="h-5 w-5 text-neon-dark group-hover:text-neon disabled:group-hover:bg-gray-600 disabled:disabled:bg-gray-600"
className="text-neon-dark group-hover:text-neon h-5 w-5 disabled:disabled:bg-gray-600 disabled:group-hover:bg-gray-600"
aria-hidden="true"
/>
</span>
@@ -140,10 +132,7 @@ export default function Login(props: any) {
</div>
<div>
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<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>
@@ -152,10 +141,7 @@ export default function Login(props: any) {
{props.allowSignup ? (
<p className="mt-2 text-center text-sm text-gray-600">
Are you new here?{" "}
<Link
href="/signup"
className="font-medium text-neon hover:text-neon"
>
<Link href="/signup" className="text-neon hover:text-neon font-medium">
Create a new Account
</Link>
</p>
@@ -164,8 +150,7 @@ export default function Login(props: any) {
Like Documenso{" "}
<Link
href="https://documenso.com"
className="font-medium text-neon hover:text-neon"
>
className="text-neon hover:text-neon font-medium">
Hosted Documenso will be availible soon
</Link>
</p>

View File

@@ -1,25 +1,16 @@
import { classNames } from "@documenso/lib";
import Link from "next/link";
import { classNames } from "@documenso/lib";
export default function Logo(props: any) {
return (
<>
<Link href="/dashboard">
<svg
className="w-12"
viewBox="0 0 88.6758041381836 32.18000030517578"
{...props}
>
<rect
width="88.6758041381836"
height="32.18000030517578"
fill="transparent"
></rect>
<svg className="w-12" viewBox="0 0 88.6758041381836 32.18000030517578" {...props}>
<rect width="88.6758041381836" height="32.18000030517578" fill="transparent"></rect>
<g transform="matrix(1,0,0,1,-25.98720359802246,-66.41200256347656)">
<path
d="M99.004,68.26l-.845-1.848-23.226,2.299c-1.437,.142-2.876,.089-4.3-.156-2.949-.51-6.32-1.717-9.073-2.099-1.477-.206-2.934,.448-3.789,1.67-1.174,1.678-2.308,3.888-3.501,5.622-1.518,2.207-3.032,4.418-4.531,6.638-1.693,2.499-3.365,5.013-5.061,7.51-.103,.153-.333,.221-.503,.329-.079-.279-.306-.636-.206-.824,1.006-1.928,1.845-4,3.165-5.694,1.908-2.449,2.914-5.445,5.007-7.745,.342-.777,.845-1.466,1.151-2.244,.915-2.341-1.295-5.305-2.935-5.305h-.613c-1.196,0-2.526,.357-4.092,1.716-.986,.856-2.391,2.432-2.97,3.326-3.424,5.286-7.177,10.382-10.15,15.932-.365,.682-1.719,3.722-2.013,3.606-.214-.077-.159-.458-.041-.823,1.312-4.065,4.163-9.851,5.843-13.777,.41-.962,.635-1.516-.305-2.361-1.016-.913-2.669,.084-3.084,1.052-1.784,4.174-2.631,8.08-4.038,12.396-.466,1.43-.916,2.865-1.386,4.294-.273,.831-.548,1.661-.836,2.488-.112,.321-.226,.642-.345,.962-.032,.082-.064,.165-.095,.248-.006,.011-.003,.002-.009,.017-.005,.008-.003,.015-.006,.023-.011,.026-.021,.05-.03,.076-.024,.067-.012,.044,.009-.002-.691,1.577,.417,3.002,2.091,3.002,.808,0,1.552-1.431,1.943-2.055,1.224-1.957,3.28-5.125,4.36-7.163,.894-1.69,2.078-3.14,3.209-4.615,1.857-2.42,3.065-5.242,5.025-7.575,.33-.394,.694-.772,1.093-1.093,.121-.098,.473-.05,.618,.062,.124,.098,.2,.432,.127,.577-.397,.788-.863,1.542-1.278,2.322-1.481,2.79-2.953,5.582-4.425,8.377-1.16,2.204-2.659,5.055-3.551,6.755-.438,.833-.176,1.857,.604,2.382,0,0-.383,2.028,1.908,2.028,1.896,0,2.711-1.145,3.053-1.624,.348-.486,.748-.951,1.202-1.334,.151-.13,.563-.018,.821,.079,.076,.029,.085,.406,.03,.582-.398,1.272-.323,2.283,1.879,2.283,.784,0,2.218-1.283,2.904-2.213,.794-1.075,.731-1.1,1.415-2.244,1.678-2.821,3.132-5.055,4.751-7.91,1.083-1.91,2.175-3.854,3.294-5.516,.685-1.015,1.446-1.252,2.188-1.252s.409,.686,.336,1.025c-.071,.341-.677,1.545-2.531,4.35-1.115,1.687-2.244,3.367-3.308,5.084-1.075,1.736-2.141,3.48-3.205,5.225-.88,1.445,.162,3.467,1.854,3.467h2.765c2.653,0,5.302-.397,7.916-.851l7.41-1.287c1.421-.245,2.868-.298,4.303-.158l23.161,2.296,.845-1.849c4.61-9.858,15.211-11.322,15.66-11.38v-5.717c-.448-.058-11.05-1.522-15.66-11.381Zm-40.143,6.165c-.164,.465-.491,.922-.86,1.255-.918,.828-1.451,1.842-1.911,2.977-.512,1.269-1.829,2.119-1.966,3.65-.027,.3-.627,.577-1.006,.797-.109,.062-.357-.114-.524-.174,.015-.229-.024-.401,.039-.515,.848-1.495,1.678-3.002,2.584-4.462,.775-1.254,1.642-2.453,2.469-3.677,.085-.126,.158-.273,.27-.365,.457-.368,.93-.719,1.399-1.075,.497,.721-.312,1.071-.494,1.589Zm30.047,12.011c1.736,0,3.162-1.137,3.686-2.693h10.547c-3.02,1.916-6.1,4.684-8.402,8.716l-21.902-2.17v-15.584l21.902-2.167c2.302,4.032,5.382,6.802,8.402,8.714h-10.547c-.524-1.554-1.951-2.691-3.686-2.691-2.172,0-3.938,1.763-3.938,3.938s1.766,3.938,3.938,3.938Z"
className={classNames(props.dark ? "fill-white" : "fill-brown")}
></path>
className={classNames(props.dark ? "fill-white" : "fill-brown")}></path>
</g>
</svg>
</Link>

View File

@@ -1,23 +1,22 @@
import { Fragment, useEffect, useState } from "react";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { signOut, useSession } from "next-auth/react";
import avatarFromInitials from "avatar-from-initials";
import { toast } from "react-hot-toast";
import { getUser } from "@documenso/lib/api";
import Logo from "./logo";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import {
ArrowRightOnRectangleIcon,
Bars3Icon,
BellIcon,
XMarkIcon,
UserCircleIcon,
ArrowRightOnRectangleIcon,
DocumentTextIcon,
ChartBarIcon,
DocumentTextIcon,
UserCircleIcon,
WrenchIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import Logo from "./logo";
import { getUser } from "@documenso/lib/api";
import avatarFromInitials from "avatar-from-initials";
import { signOut, useSession } from "next-auth/react";
import { toast } from "react-hot-toast";
const navigation = [
{
@@ -125,14 +124,12 @@ export default function TopNavigation() {
item.current
? "border-neon text-brown"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
"inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium"
)}
aria-current={item.current ? "page" : undefined}
>
aria-current={item.current ? "page" : undefined}>
<item.icon
className="flex-shrink-0 -ml-1 mr-3 h-6 w-6 inline"
aria-hidden="true"
></item.icon>
className="-ml-1 mr-3 inline h-6 w-6 flex-shrink-0"
aria-hidden="true"></item.icon>
{item.name}
</Link>
))}
@@ -142,8 +139,7 @@ export default function TopNavigation() {
onClick={() => {
document?.getElementById("mb")?.click();
}}
className="hidden sm:ml-6 sm:flex sm:items-center hover:bg-gray-200 px-3 cursor-pointer"
>
className="hidden cursor-pointer px-3 hover:bg-gray-200 sm:ml-6 sm:flex sm:items-center">
<span className="text-sm">
<p className="font-bold">{user?.name || ""}</p>
<p>{user?.email}</p>
@@ -152,8 +148,7 @@ export default function TopNavigation() {
<div>
<Menu.Button
id="mb"
className="flex max-w-xs items-center rounded-full bg-white text-sm"
>
className="flex max-w-xs items-center rounded-full bg-white text-sm">
<span className="sr-only">Open user menu</span>
<div
key={user?.email}
@@ -170,8 +165,7 @@ export default function TopNavigation() {
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
leaveTo="transform opacity-0 scale-95">
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
@@ -182,12 +176,10 @@ export default function TopNavigation() {
className={classNames(
active ? "bg-gray-100" : "",
"block px-4 py-2 text-sm text-gray-700"
)}
>
)}>
<item.icon
className="flex-shrink-0 -ml-1 mr-3 h-6 w-6 inline"
aria-hidden="true"
></item.icon>
className="-ml-1 mr-3 inline h-6 w-6 flex-shrink-0"
aria-hidden="true"></item.icon>
{item.name}
</Link>
)}
@@ -219,15 +211,14 @@ export default function TopNavigation() {
href={item.href}
className={classNames(
item.current
? "bg-teal-50 border-teal-500 text-teal-700"
: "border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800",
"block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
? "border-teal-500 bg-teal-50 text-teal-700"
: "border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800",
"block border-l-4 py-2 pl-3 pr-4 text-base font-medium"
)}
aria-current={item.current ? "page" : undefined}
onClick={() => {
close();
}}
>
}}>
{item.name}
</Link>
))}
@@ -259,8 +250,7 @@ export default function TopNavigation() {
: item.click
}
href={item.href}
className="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800"
>
className="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800">
{item.name}
</Link>
))}

View File

@@ -1,12 +1,12 @@
import { ChangeEvent, useEffect, useState } from "react";
import { KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import Link from "next/link";
import Head from "next/head";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { updateUser } from "@documenso/features";
import { Button } from "@documenso/ui";
import { getUser } from "@documenso/lib/api";
import { Button } from "@documenso/ui";
import { KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
import { useSession } from "next-auth/react";
const subNavigation = [
{
@@ -74,15 +74,12 @@ export default function Setttings() {
</Head>
<header className="py-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold leading-tight tracking-tight text-brown">
Settings
</h1>
<h1 className="text-brown text-3xl font-bold leading-tight tracking-tight">Settings</h1>
</div>
</header>
<div
className="mx-auto max-w-screen-xl px-4 pb-6 sm:px-6 lg:px-8 lg:pb-16"
hidden={!user.email}
>
hidden={!user.email}>
<div className="overflow-hidden rounded-lg bg-white shadow">
<div className="divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
<aside className="py-6 lg:col-span-3">
@@ -93,18 +90,17 @@ export default function Setttings() {
href={item.href}
className={classNames(
item.current
? "bg-teal-50 border-neon-dark text-teal-700 hover:bg-teal-50 hover:text-teal-700"
? "border-neon-dark bg-teal-50 text-teal-700 hover:bg-teal-50 hover:text-teal-700"
: "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900",
"group border-l-4 px-3 py-2 flex items-center text-sm font-medium"
"group flex items-center border-l-4 px-3 py-2 text-sm font-medium"
)}
aria-current={item.current ? "page" : undefined}
>
aria-current={item.current ? "page" : undefined}>
<item.icon
className={classNames(
item.current
? "text-teal-500 group-hover:text-teal-500"
: "text-gray-400 group-hover:text-gray-500",
"flex-shrink-0 -ml-1 mr-3 h-6 w-6"
"-ml-1 mr-3 h-6 w-6 flex-shrink-0"
)}
aria-hidden="true"
/>
@@ -115,20 +111,14 @@ export default function Setttings() {
</aside>
<form
className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9"
action="#"
method="POST"
hidden={
subNavigation.filter((e) => e.current)[0]?.name !==
subNavigation[0].name
}
>
hidden={subNavigation.filter((e) => e.current)[0]?.name !== subNavigation[0].name}>
{/* Profile 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">
Profile
</h2>
<h2 className="text-lg font-medium leading-6 text-gray-900">Profile</h2>
<p className="mt-1 text-sm text-gray-500">
Let people know who they are dealing with builds trust.
</p>
@@ -136,10 +126,7 @@ export default function Setttings() {
<div className="my-6 grid grid-cols-12 gap-6">
<div className="col-span-12 sm:col-span-6">
<label
htmlFor="first-name"
className="block text-sm font-medium text-gray-700"
>
<label htmlFor="first-name" className="block text-sm font-medium text-gray-700">
Full Name
</label>
<input
@@ -150,14 +137,11 @@ export default function Setttings() {
onChange={(e) => handleNameChange(e)}
onKeyDown={handleKeyPress}
autoComplete="given-name"
className="mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:outline-none sm:text-sm"
/>
</div>
<div className="col-span-12 sm:col-span-6">
<label
htmlFor="first-name"
className="block text-sm font-medium text-gray-700"
>
<label htmlFor="first-name" className="block text-sm font-medium text-gray-700">
Email
</label>
<input
@@ -167,7 +151,7 @@ export default function Setttings() {
name="first-name"
id="first-name"
autoComplete="given-name"
className="mt-1 block w-full rounded-md border disabled:bg-neutral-100 border-gray-300 py-2 px-3 shadow-sm focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:outline-none disabled:bg-neutral-100 sm:text-sm"
/>
</div>
</div>
@@ -175,21 +159,14 @@ export default function Setttings() {
</div>
</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]"
>
hidden={subNavigation.filter((e) => e.current)[0]?.name !== subNavigation[1].name}
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9">
{/* 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>
<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.
Forgot your passwort? Email <b>hi@documenso.com</b> to reset it.
</p>
</div>
</div>

View File

@@ -1,9 +1,9 @@
import Link from "next/link";
import { signup } from "@documenso/lib/api";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { Button } from "@documenso/ui";
import { XCircleIcon } from "@heroicons/react/24/outline";
import { signIn } from "next-auth/react";
import Link from "next/link";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
@@ -107,8 +107,7 @@ export default function Signup(props: { source: string }) {
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="rgb(17 24 39 / var(--tw-text-opacity))"
className="w-8 h-8 inline mb-1"
>
className="mb-1 inline h-8 w-8">
<path
strokeLinecap="round"
strokeLinejoin="round"
@@ -130,8 +129,7 @@ export default function Signup(props: { source: string }) {
form.clearErrors();
trigger();
}}
className="mt-8 space-y-6"
>
className="mt-8 space-y-6">
<input type="hidden" name="remember" defaultValue="true" />
<div className="-space-y-px rounded-md shadow-sm">
<div>
@@ -145,7 +143,7 @@ export default function Signup(props: { source: string }) {
type="email"
autoComplete="email"
required
className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="Email"
/>
</div>
@@ -157,8 +155,7 @@ export default function Signup(props: { source: string }) {
{...register("password", {
minLength: {
value: 7,
message:
"Your password has to be at least 7 characters long.",
message: "Your password has to be at least 7 characters long.",
},
})}
id="password"
@@ -166,7 +163,7 @@ export default function Signup(props: { source: string }) {
type="password"
autoComplete="current-password"
required
className="relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="Password"
/>
</div>
@@ -177,16 +174,12 @@ export default function Signup(props: { source: string }) {
onClick={() => {
form.clearErrors();
}}
className="sgroup relative flex w-full"
>
className="sgroup relative flex w-full">
Create Account
</Button>
<div className="pt-2">
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<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>
@@ -194,10 +187,7 @@ export default function Signup(props: { source: string }) {
</div>
<p className="mt-2 text-center text-sm text-gray-600">
Already have an account?{" "}
<Link
href="/login"
className="font-medium text-neon hover:text-neon"
>
<Link href="/login" className="text-neon hover:text-neon font-medium">
Sign In
</Link>
</p>

View File

@@ -18,7 +18,6 @@ const withTM = require("next-transpile-modules")([
const plugins = [];
plugins.push(withTM);
const moduleExports = () =>
plugins.reduce((acc, next) => next(acc), nextConfig);
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
module.exports = moduleExports;

View File

@@ -1,31 +1,29 @@
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
import { Button } from "@documenso/ui";
import Logo from "../components/logo";
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
export default function Custom404() {
return (
<>
<main className="relative min-h-full bg-gray-100 isolate">
<main className="relative isolate min-h-full bg-gray-100">
<div className="absolute top-10 left-10">
<Logo className="w-10 md:w-20" />
</div>
<div className="px-6 py-48 mx-auto text-center max-w-7xl sm:py-40 lg:px-8">
<p className="text-base font-semibold leading-8 text-brown">404</p>
<h1 className="mt-4 text-3xl font-bold tracking-tight text-brown sm:text-5xl">
<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 couldnt find the page youre looking for.
</p>
<div className="flex justify-center mt-10">
<div className="mt-10 flex justify-center">
<Button
color="secondary"
href="/"
icon={ArrowSmallLeftIcon}
className="text-base font-semibold leading-7 text-brown"
>
className="text-brown text-base font-semibold leading-7">
Back to home
</Button>
</div>

View File

@@ -1,27 +1,25 @@
import Logo from "../components/logo";
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 flex-col items-center justify-center min-h-full text-white bg-black">
<div className="relative flex min-h-full flex-col items-center justify-center bg-black text-white">
<div className="absolute top-10 left-10">
<Logo dark className="w-10 md:w-20" />
</div>
<div className="px-4 py-10 mt-20 max-w-7xl">
<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 px-3 font-thin sm:text-6xl -top-1.5">
|
</span>{" "}
<span className="text-base font-semibold align-middle sm:text-2xl">
<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="flex justify-center mt-10">
<div className="mt-10 flex justify-center">
<Button color="secondary" href="/" icon={ArrowSmallLeftIcon}>
Back to home
</Button>

View File

@@ -1,13 +1,14 @@
import "../styles/tailwind.css";
import { ReactElement, ReactNode } from "react";
import { NextPage } from "next";
import type { AppProps } from "next/app";
import "../../../node_modules/placeholder-loading/src/scss/placeholder-loading.scss";
import "../../../node_modules/react-resizable/css/styles.css";
import "react-tooltip/dist/react-tooltip.css";
import { ReactElement, ReactNode } from "react";
import type { AppProps } from "next/app";
import { NextPage } from "next";
import "../styles/tailwind.css";
import { SessionProvider } from "next-auth/react";
export { coloredConsole } from "@documenso/lib";
import { Toaster } from "react-hot-toast";
import "react-tooltip/dist/react-tooltip.css";
export { coloredConsole } from "@documenso/lib";
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;

View File

@@ -5,10 +5,7 @@ export default function Document(props) {
let pageProps = props.__NEXT_DATA__?.props?.pageProps;
return (
<Html
className="h-full bg-gray-100 scroll-smooth font-normal antialiased"
lang="en"
>
<Html className="h-full scroll-smooth bg-gray-100 font-normal antialiased" lang="en">
<Head>
<meta name="color-scheme"></meta>
</Head>

View File

@@ -1,9 +1,9 @@
import NextAuth, { Session } from "next-auth";
import GitHubProvider from "next-auth/providers/github";
import CredentialsProvider from "next-auth/providers/credentials";
import { ErrorCode } from "@documenso/lib/auth";
import prisma from "@documenso/prisma";
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,
@@ -27,8 +27,7 @@ export default NextAuth({
password: {
label: "Password",
type: "password",
placeholder:
"Select a password. Here is some inspiration: https://xkcd.com/936/",
placeholder: "Select a password. Here is some inspiration: https://xkcd.com/936/",
},
},
async authorize(credentials: any) {
@@ -57,10 +56,7 @@ export default NextAuth({
throw new Error(ErrorCode.UserMissingPassword);
}
const isCorrectPassword = await verifyPassword(
credentials.password,
user.password
);
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
if (!isCorrectPassword) {
throw new Error(ErrorCode.IncorrectPassword);

View File

@@ -1,9 +1,8 @@
import { IdentityProvider } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@documenso/prisma";
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;

View File

@@ -1,13 +1,9 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { Document as PrismaDocument } from "@prisma/client";
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;
@@ -46,8 +42,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
document = await getDocument(+documentId, req, res);
}
if (!document)
res.status(404).end(`No document with id ${documentId} found.`);
if (!document) res.status(404).end(`No document with id ${documentId} found.`);
const signaturesCount = await prisma.signature.count({
where: {
@@ -61,18 +56,13 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
// No need to add a signature, if no one signed yet.
if (signaturesCount > 0) {
signedDocumentAsBase64 = await addDigitalSignature(
document?.document || ""
);
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}`
);
res.setHeader("Content-Disposition", `attachment; filename=${document?.title}`);
return res.status(200).send(buffer);
}

View File

@@ -1,13 +1,9 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import short from "short-uuid";
import { Document as PrismaDocument, FieldType } from "@prisma/client";
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);

View File

@@ -1,12 +1,8 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { Document as PrismaDocument, FieldType } from "@prisma/client";
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);
@@ -61,18 +57,14 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
});
if (!recipient || recipient?.documentId !== +documentId)
return res
.status(401)
.send("Recipient does not have access to this document.");
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.");
return res.status(401).send("User does not have access to this document.");
}
}

View File

@@ -1,13 +1,9 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import short from "short-uuid";
import { Document as PrismaDocument } from "@prisma/client";
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);

View File

@@ -1,13 +1,9 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import short from "short-uuid";
import { Document as PrismaDocument } from "@prisma/client";
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);

View File

@@ -1,12 +1,8 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
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) {
@@ -23,8 +19,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const document: PrismaDocument = await getDocument(+documentId, req, res);
if (!document)
res.status(404).end(`No document with id ${documentId} found.`);
if (!document) res.status(404).end(`No document with id ${documentId} found.`);
let recipientCondition: any = {
documentId: +documentId,

View File

@@ -1,11 +1,11 @@
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { SigningStatus, DocumentStatus } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { Document as PrismaDocument, FieldType } from "@prisma/client";
import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
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;
@@ -73,6 +73,13 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
},
});
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({
@@ -115,10 +122,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
},
data: {
document: documentWithInserts,
status:
unsignedRecipients.length > 0
? DocumentStatus.PENDING
: DocumentStatus.COMPLETED,
status: unsignedRecipients.length > 0 ? DocumentStatus.PENDING : DocumentStatus.COMPLETED,
},
});
@@ -129,8 +133,11 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
});
document.document = documentWithInserts;
if (documentOwner)
await sendSigningDoneMail(recipient, document, documentOwner);
if (documentOwner) await sendSigningDoneMail(document, documentOwner);
for (const signer of signedRecipients) {
await sendSigningDoneMail(document, signer);
}
}
return res.status(200).end();
@@ -139,9 +146,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
if (signedField?.Signature?.signatureImageAsBase64) {
documentWithInserts = await insertImageInPDF(
documentWithInserts,
signedField.Signature
? signedField.Signature?.signatureImageAsBase64
: "",
signedField.Signature ? signedField.Signature?.signatureImageAsBase64 : "",
signedField.positionX,
signedField.positionY,
signedField.page
@@ -169,12 +174,8 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
create: {
recipientId: recipient.id,
fieldId: signature.fieldId,
signatureImageAsBase64: signature.signatureImage
? signature.signatureImage
: null,
typedSignature: signature.typedSignature
? signature.typedSignature
: null,
signatureImageAsBase64: signature.signatureImage ? signature.signatureImage : null,
typedSignature: signature.typedSignature ? signature.typedSignature : null,
},
});
}

View File

@@ -1,9 +1,9 @@
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { getUserFromToken } from "@documenso/lib/server";
import formidable from "formidable";
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";
export const config = {
api: {

View File

@@ -1,5 +1,4 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";

View File

@@ -1,8 +1,9 @@
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import { NextApiRequest, NextApiResponse } from "next";
import { Document as PrismaDocument } from "@prisma/client";
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
@@ -12,10 +13,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
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}`
);
res.setHeader("Content-Disposition", `attachment; filename=${document.title}`);
return res.status(200).send(signedDocument);
}

View File

@@ -1,11 +1,6 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import type { NextApiRequest, NextApiResponse } from "next";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { method, body } = req;

View File

@@ -1,11 +1,6 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
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);

View File

@@ -1,7 +1,10 @@
import Head from "next/head";
import { ReactElement } from "react";
import Layout from "../components/layout";
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,
@@ -9,15 +12,12 @@ import {
ExclamationTriangleIcon,
UsersIcon,
} from "@heroicons/react/24/outline";
import { uploadDocument } from "@documenso/features";
import {
DocumentStatus,
Document as PrismaDocument,
SendStatus,
SigningStatus,
Document as PrismaDocument,
} from "@prisma/client";
import { getUserFromToken } from "@documenso/lib/server";
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
import { truncate } from "fs";
import { Tooltip as ReactTooltip } from "react-tooltip";
@@ -59,15 +59,14 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
Dashboard
</h1>
</header>
<dl className="grid gap-5 mt-8 md:grid-cols-3 ">
<dl className="mt-8 grid gap-5 md:grid-cols-3 ">
{stats.map((item) => (
<Link href={item.link} key={item.name}>
<div className="px-4 py-3 overflow-hidden bg-white rounded-lg shadow md:p-6 sm:py-5">
<dt className="text-sm font-medium text-gray-500 truncate ">
<div className="overflow-hidden rounded-lg bg-white px-4 py-3 shadow sm:py-5 md:p-6">
<dt className="truncate text-sm font-medium text-gray-500 ">
<item.icon
className="flex-shrink-0 inline w-5 h-5 mr-3 text-neon sm:w-6 sm:h-6"
aria-hidden="true"
></item.icon>
className="text-neon 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-1 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
@@ -92,25 +91,20 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
onClick={() => {
document?.getElementById("fileUploadHelper")?.click();
}}
className="relative block w-full p-12 text-center border-2 border-gray-300 border-dashed rounded-lg cursor-pointer hover:border-neon focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
className="hover:border-neon 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">
<svg
className="w-12 h-12 mx-auto text-gray-400"
className="mx-auto h-12 w-12 text-gray-400"
stroke="currentColor"
fill="none"
viewBox="0 00 20 25"
aria-hidden="true"
>
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="mt-2 block text-sm font-medium text-neon"
>
<span id="add_document" className="text-neon mt-2 block text-sm font-medium">
Add a new PDF document.
</span>
</div>
@@ -147,9 +141,7 @@ export async function getServerSideProps(context: any) {
const documents: any[] = await getDocumentsForUserFromToken(context);
const drafts: PrismaDocument[] = documents.filter(
(d) => d.status === DocumentStatus.DRAFT
);
const drafts: PrismaDocument[] = documents.filter((d) => d.status === DocumentStatus.DRAFT);
const waiting: any[] = documents.filter(
(e) =>

View File

@@ -1,7 +1,12 @@
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 { Button, IconButton, SelectBox } from "@documenso/ui";
import Layout from "../components/layout";
import type { NextPageWithLayout } from "./_app";
import Head from "next/head";
import {
ArrowDownTrayIcon,
CheckBadgeIcon,
@@ -13,12 +18,7 @@ import {
PlusIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import { uploadDocument } from "@documenso/features";
import { DocumentStatus } from "@prisma/client";
import { Button, IconButton, SelectBox } from "@documenso/ui";
import { NextPageContext } from "next";
import { deleteDocument, getDocuments } from "@documenso/lib/api";
import { Tooltip as ReactTooltip } from "react-tooltip";
const DocumentsPage: NextPageWithLayout = (props: any) => {
@@ -42,12 +42,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
{ label: "Last 12 months", value: 366 },
];
const [selectedStatusFilter, setSelectedStatusFilter] = useState(
statusFilters[0]
);
const [selectedCreatedFilter, setSelectedCreatedFilter] = useState(
createdFilter[0]
);
const [selectedStatusFilter, setSelectedStatusFilter] = useState(statusFilters[0]);
const [selectedCreatedFilter, setSelectedCreatedFilter] = useState(createdFilter[0]);
const loadDocuments = async () => {
if (!documents.length) setLoading(true);
@@ -62,9 +58,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
useEffect(() => {
loadDocuments().finally(() => {
setSelectedStatusFilter(
statusFilters.filter(
(status) => status.value === props.filter.toUpperCase()
)[0]
statusFilters.filter((status) => status.value === props.filter.toUpperCase())[0]
);
});
}, []);
@@ -79,9 +73,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
function filterDocumentes(documents: []): any {
let filteredDocuments = documents.filter(
(d: any) =>
d.status === selectedStatusFilter.value ||
selectedStatusFilter.value === "ALL"
(d: any) => d.status === selectedStatusFilter.value || selectedStatusFilter.value === "ALL"
);
filteredDocuments = filteredDocuments.filter((document: any) =>
@@ -98,9 +90,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
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
);
const diffInDays = Math.floor((today.getTime() - documentDate.getTime()) / millisecondsInDay);
console.log(diffInDays);
@@ -114,7 +104,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<title>Documents | Documenso</title>
</Head>
<div className="px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:items-center mt-10">
<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">
@@ -127,27 +117,24 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
icon={DocumentPlusIcon}
onClick={() => {
document?.getElementById("fileUploadHelper")?.click();
}}
>
}}>
Add Document
</Button>
</div>
</div>
<div className="mt-3 mb-12">
<div className="w-fit block float-right ml-3 mt-7">
{filteredDocuments.length != 1
? filteredDocuments.length + " Documents"
: "1 Document"}
<div className="float-right ml-3 mt-7 block w-fit">
{filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
</div>
<SelectBox
className="w-1/4 block float-right"
className="float-right block w-1/4"
label="Created"
options={createdFilter}
value={selectedCreatedFilter}
onChange={setSelectedCreatedFilter}
/>
<SelectBox
className="w-1/4 block float-right ml-3"
className="float-right ml-3 block w-1/4"
label="Status"
options={statusFilters}
value={selectedStatusFilter}
@@ -171,14 +158,10 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
</div>
</div>
</div>
<div
className="mt-28 flex flex-col"
hidden={!documents.length || loading}
>
<div className="mt-28 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}
>
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">
@@ -186,32 +169,25 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<tr>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
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"
>
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"
>
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"
>
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"
>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">Delete</span>
</th>
</tr>
@@ -220,9 +196,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
{filteredDocuments.map((document: any, index: number) => (
<tr
key={document.id}
className="hover:bg-gray-100 cursor-pointer"
onClick={(event) => showDocument(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>
@@ -232,26 +207,19 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
{item.sendStatus === "NOT_SENT" ? (
<span
id="sent_icon"
className="inline-block flex-shrink-0 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}
className="inline-block flex-shrink-0 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" ? (
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
<span id="sent_icon">
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800"
>
<EnvelopeIcon className="inline h-5 mr-1"></EnvelopeIcon>
{item.name
? item.name + " <" + item.email + ">"
: item.email}
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800">
<EnvelopeIcon className="mr-1 inline h-5"></EnvelopeIcon>
{item.name ? item.name + " <" + item.email + ">" : item.email}
</span>
</span>
) : (
@@ -262,13 +230,10 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<span id="read_icon">
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800"
>
<CheckIcon className="inline h-5 -mr-2"></CheckIcon>
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
{item.name
? item.name + " <" + item.email + ">"
: item.email}
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800">
<CheckIcon className="-mr-2 inline h-5"></CheckIcon>
<CheckIcon className="mr-1 inline h-5"></CheckIcon>
{item.name ? item.name + " <" + item.email + ">" : item.email}
</span>
</span>
) : (
@@ -277,7 +242,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
{item.signingStatus === "SIGNED" ? (
<span id="signed_icon">
<span className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
<CheckBadgeIcon className="inline h-5 mr-1"></CheckBadgeIcon>{" "}
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>{" "}
{item.email}
</span>
</span>
@@ -307,9 +272,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
{formatDocumentStatus(document.status)}
<p>
<small hidden={document.Recipient.length === 0}>
{document.Recipient.filter(
(r: any) => r.signingStatus === "SIGNED"
).length || 0}
{document.Recipient.filter((r: any) => r.signingStatus === "SIGNED")
.length || 0}
/{document.Recipient.length || 0}
</small>
</p>
@@ -327,6 +291,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
event.stopPropagation();
router.push("/documents/" + document.id);
}}
disabled={document.status === "COMPLETED"}
/>
<IconButton
icon={ArrowDownTrayIcon}
@@ -342,30 +307,20 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
onClick={(event: any) => {
event.preventDefault();
event.stopPropagation();
if (
confirm(
"Are you sure you want to delete this document"
)
) {
if (confirm("Are you sure you want to delete this document")) {
const documentsWithoutIndex = [...documents];
const removedItem: any =
documentsWithoutIndex.splice(index, 1);
const removedItem: any = documentsWithoutIndex.splice(index, 1);
setDocuments(documentsWithoutIndex);
deleteDocument(document.id)
.catch((err) => {
documentsWithoutIndex.splice(
index,
0,
removedItem
);
documentsWithoutIndex.splice(index, 0, removedItem);
setDocuments(documentsWithoutIndex);
})
.then(() => {
loadDocuments();
});
}
}}
></IconButton>
}}></IconButton>
<span className="sr-only">, {document.name}</span>
</div>
</td>
@@ -374,29 +329,21 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
</tbody>
</table>
</div>
<div
hidden={filteredDocuments.length > 0}
className="mx-auto w-fit mt-12 p-3"
>
<FunnelIcon className="w-5 inline mr-1 align-middle" /> Nothing
here. Maybe try a different filter.
<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="text-center mt-24"
id="empty"
hidden={documents.length > 0 || loading}
>
<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"
>
aria-hidden="true">
<path
strokeLinecap="round"
strokeLinejoin="round"
@@ -413,8 +360,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
icon={PlusIcon}
onClick={() => {
document?.getElementById("fileUploadHelper")?.click();
}}
>
}}>
Add Document
</Button>
<input

View File

@@ -1,20 +1,16 @@
import { ReactElement } from "react";
import Layout from "../../../components/layout";
import { NextPageWithLayout } from "../../_app";
import Link from "next/link";
import { useRouter } from "next/router";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
import { getUserFromToken } from "@documenso/lib/server";
import Link from "next/link";
import { DocumentStatus } from "@prisma/client";
import {
InformationCircleIcon,
PaperAirplaneIcon,
UsersIcon,
} from "@heroicons/react/24/outline";
import { getDocument } from "@documenso/lib/query";
import { Document as PrismaDocument } from "@prisma/client";
import { Button, Breadcrumb } from "@documenso/ui";
import { getUserFromToken } from "@documenso/lib/server";
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();
@@ -32,8 +28,7 @@ const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
},
{
title: props.document.title,
href:
NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
},
]}
/>
@@ -67,21 +62,13 @@ const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
<Button
icon={PaperAirplaneIcon}
className="ml-3"
href={
NEXT_PUBLIC_WEBAPP_URL +
"/documents/" +
props.document.id +
"/recipients"
}
href={NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients"}
onClick={() => {
if (
confirm(
`Send document out to ${props?.document?.Recipient?.length} recipients?`
)
confirm(`Send document out to ${props?.document?.Recipient?.length} recipients?`)
) {
}
}}
>
}}>
Prepare to Send
</Button>
</div>
@@ -120,11 +107,7 @@ export async function getServerSideProps(context: any) {
const { id: documentId } = context.query;
try {
const document: PrismaDocument = await getDocument(
+documentId,
context.req,
context.res
);
const document: PrismaDocument = await getDocument(+documentId, context.req, context.res);
return {
props: {

View File

@@ -1,29 +1,29 @@
import Head from "next/head";
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 } from "@documenso/ui";
import Layout from "../../../components/layout";
import { NextPageWithLayout } from "../../_app";
import { classNames, NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
import {
ArrowDownTrayIcon,
CheckBadgeIcon,
CheckIcon,
EnvelopeIcon,
PaperAirplaneIcon,
PencilSquareIcon,
TrashIcon,
UserPlusIcon,
EnvelopeIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { getUserFromToken } from "@documenso/lib/server";
import { getDocument } from "@documenso/lib/query";
import { Document as PrismaDocument, DocumentStatus } from "@prisma/client";
import { Breadcrumb, Button, Dialog, IconButton } from "@documenso/ui";
import { createOrUpdateRecipient, deleteRecipient, sendSigningRequests } from "@documenso/lib/api";
import { DocumentStatus, Document as PrismaDocument, Recipient } from "@prisma/client";
import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form";
import { toast } from "react-hot-toast";
export type FormValues = {
signers: { id: number; email: string; name: string }[];
signers: Array<Pick<Recipient, 'id' | 'email' | 'name' | 'sendStatus'>>;
};
const RecipientsPage: NextPageWithLayout = (props: any) => {
@@ -35,7 +35,10 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
},
{
title: props.document.title,
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
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",
@@ -71,80 +74,84 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
<Head>
<title>{title}</title>
</Head>
<div className="px-6 mt-10 sm:px-0">
<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="flex-1 min-w-0">
<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="flex flex-shrink-0 mt-4 md:mt-0 md:ml-4">
<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}
>
href={"/api/documents/" + props.document.id}>
Download
</Button>
<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={() => {
setOpen(true);
}}
disabled={
(formValues.length || 0) === 0 ||
!formValues.some(
(r: any) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
) ||
loading
}
>
Send
</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={
(formValues.length || 0) === 0 ||
!formValues.some(
(r) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
) ||
loading
}>
Send
</Button>
</>
)}
</div>
</div>
<div className="p-4 mt-10 overflow-hidden bg-white rounded-md shadow sm:p-6">
<div className="pb-3 border-b border-gray-200 sm:pb-5">
<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="max-w-4xl mt-2 text-sm text-gray-500">
The people who will sign the document.
<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: any, index: number) => (
<li
key={index}
className="w-full px-2 py-3 border-0 hover:bg-green-50 group sm:py-4"
>
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:space-x-2 md:space-y-0 md:flex">
<div className="block space-y-2 md:flex md:space-x-2 md:space-y-0">
<div
className={classNames(
"md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
"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>
@@ -170,8 +177,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
documentId: props.document.id,
});
}}
className="block w-full p-0 text-gray-900 placeholder-gray-500 disabled:bg-neutral-100 border-0 outline-none sm:text-sm bg-inherit"
placeholder="john.dorian@loremipsum.com"
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">
@@ -183,10 +189,9 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
</div>
<div
className={classNames(
"md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
"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>
@@ -209,121 +214,110 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
documentId: props.document.id,
});
}}
className="block w-full p-0 text-gray-900 placeholder-gray-500 disabled:bg-neutral-100 border-0 outline-none sm:text-sm bg-inherit"
placeholder="John Dorian"
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="flex mb-2 mr-2 lg:mr-0">
<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="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
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="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800 "
>
<CheckIcon className="inline h-5 mr-1" /> Sent
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="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
<CheckIcon className="inline h-5 -mr-2"></CheckIcon>
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
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="inline-block mt-3 flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"
>
<CheckBadgeIcon className="inline h-5 mr-1"></CheckBadgeIcon>
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>
<div className="flex mr-1">
<IconButton
icon={PaperAirplaneIcon}
disabled={
!item.id ||
item.sendStatus !== "SENT" ||
item.signingStatus === "SIGNED" ||
loading
}
color="secondary"
className="my-auto mr-4 h-9"
onClick={() => {
if (confirm("Resend this signing request?")) {
setLoading(true);
sendSigningRequests(props.document, [item.id]).finally(() => {
setLoading(false);
});
{props.document.status !== DocumentStatus.COMPLETED && (
<div className="mr-1 flex">
<IconButton
icon={PaperAirplaneIcon}
disabled={
!item.id ||
item.sendStatus !== "SENT" ||
item.signingStatus === "SIGNED" ||
loading
}
}}
>
Resend
</IconButton>
<IconButton
icon={TrashIcon}
disabled={!item.id || item.sendStatus === "SENT" || loading}
onClick={() => {
const removedItem = { ...fields }[index];
remove(index);
deleteRecipient(item)?.catch((err) => {
append(removedItem);
});
}}
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
/>
</div>
color="secondary"
className="my-auto mr-4 h-9"
onClick={() => {
if (confirm("Resend this signing request?")) {
setLoading(true);
sendSigningRequests(props.document, [item.id]).finally(() => {
setLoading(false);
});
}
}}>
Resend
</IconButton>
<IconButton
icon={TrashIcon}
disabled={!item.id || item.sendStatus === "SENT" || loading}
onClick={() => {
const removedItem = { ...fields }[index];
remove(index);
deleteRecipient(item)?.catch((err) => {
append(removedItem);
});
}}
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
/>
</div>
)}
</div>
</div>
</li>
))}
</ul>
<Button
icon={UserPlusIcon}
className="mt-3"
onClick={() => {
createOrUpdateRecipient({
id: "",
email: "",
name: "",
documentId: props.document.id,
}).then((res) => {
append(res);
});
}}
>
Add Signer
</Button>
{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>
@@ -336,7 +330,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
open={open}
setLoading={setLoading}
setOpen={setOpen}
icon={<EnvelopeIcon className="w-6 h-6 text-green-600" aria-hidden="true" />}
icon={<EnvelopeIcon className="h-6 w-6 text-green-600" aria-hidden="true" />}
/>
</>
);

View File

@@ -1,11 +1,11 @@
import prisma from "@documenso/prisma";
import Head from "next/head";
import { NextPageWithLayout } from "../../_app";
import { ReadStatus } from "@prisma/client";
import PDFSigner from "../../../components/editor/pdf-signer";
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 { FieldType, DocumentStatus } from "@prisma/client";
import { ReadStatus } from "@prisma/client";
import { DocumentStatus, FieldType } from "@prisma/client";
const SignPage: NextPageWithLayout = (props: any) => {
return (
@@ -14,36 +14,22 @@ const SignPage: NextPageWithLayout = (props: any) => {
<title>Sign | Documenso</title>
</Head>
{!props.expired ? (
<PDFSigner
document={props.document}
recipient={props.recipient}
fields={props.fields}
/>
<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 w-10 inline mr-1"></ClockIcon>
<h1 className="text-base font-medium text-neon inline align-middle">
Time flies.
</h1>
<p className="mt-2 text-4xl font-bold tracking-tight">
This signing link is expired.
</p>
<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`}{" "}
Please ask {props.document.User.name ? `${props.document.User.name}` : `the sender`}{" "}
to resend it.
</p>
<div className="mx-auto w-fit text-xl pt-20"></div>
<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="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>
@@ -51,10 +37,7 @@ const SignPage: NextPageWithLayout = (props: any) => {
</div>
<p className="mt-4 text-center text-sm text-gray-600">
Want to send of your own?{" "}
<Link
href="/signup?source=expired"
className="font-medium text-neon hover:text-neon"
>
<Link href="/signup?source=expired" className="text-neon hover:text-neon font-medium">
Create your own Account
</Link>
</p>
@@ -118,13 +101,9 @@ export async function getServerSideProps(context: any) {
return {
props: {
recipient: JSON.parse(JSON.stringify(recipient)),
document: JSON.parse(
JSON.stringify({ ...recipient.Document, document: "" })
),
document: JSON.parse(JSON.stringify({ ...recipient.Document, document: "" })),
fields: JSON.parse(JSON.stringify(unsignedFields)),
expired: recipient.expired
? new Date(recipient.expired) < new Date()
: false,
expired: recipient.expired ? new Date(recipient.expired) < new Date() : false,
},
};
}

View File

@@ -1,10 +1,10 @@
import prisma from "@documenso/prisma";
import Head from "next/head";
import { NextPageWithLayout } from "../../_app";
import { ArrowDownTrayIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
import { Button, IconButton } from "@documenso/ui";
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";
const Signed: NextPageWithLayout = (props: any) => {
const router = useRouter();
@@ -18,29 +18,18 @@ const Signed: NextPageWithLayout = (props: any) => {
<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 w-10 inline mr-1"></CheckBadgeIcon>
<h1 className="text-base font-medium text-neon inline align-middle">
It's done!
</h1>
<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 "{props.document.title}"
</p>
<p
className="mt-2 text-base text-gray-500 max-w-sm"
hidden={allRecipientsSigned}
>
<p className="mt-2 max-w-sm text-base text-gray-500" hidden={allRecipientsSigned}>
You will be notfied when all recipients have signed.
</p>
<p
className="mt-2 text-base text-gray-500 max-w-sm"
hidden={!allRecipientsSigned}
>
<p className="mt-2 max-w-sm text-base text-gray-500" hidden={!allRecipientsSigned}>
All recipients signed.
</p>
<div
className="mx-auto w-fit text-xl pt-20"
hidden={!allRecipientsSigned}
>
<div className="mx-auto w-fit pt-20 text-xl" hidden={!allRecipientsSigned}>
<Button
icon={ArrowDownTrayIcon}
color="secondary"
@@ -48,23 +37,16 @@ const Signed: NextPageWithLayout = (props: any) => {
event.preventDefault();
event.stopPropagation();
router.push(
"/api/documents/" +
props.document.id +
"?token=" +
props.recipient.token
"/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="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>
@@ -72,10 +54,7 @@ const Signed: NextPageWithLayout = (props: any) => {
</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="font-medium text-neon hover:text-neon"
>
<Link href="https://documenso.com" className="text-neon hover:text-neon font-medium">
Hosted Documenso is coming soon
</Link>
</p>

View File

@@ -1,2 +1,3 @@
import SettingsPage from ".";
export default SettingsPage;

View File

@@ -1,2 +1,3 @@
import SettingsPage from ".";
export default SettingsPage;

View File

@@ -1,2 +1,3 @@
import SettingsPage from ".";
export default SettingsPage;

View File

@@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};

View File

@@ -24,9 +24,8 @@ body,
font-weight: 400;
font-display: swap;
src: url("/fonts/montserrat.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin */
@@ -36,7 +35,6 @@ body,
font-weight: 700;
font-display: swap;
src: url("/fonts/montserrat.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

50
docker/Dockerfile Normal file
View File

@@ -0,0 +1,50 @@
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS production_deps
WORKDIR /app
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
# Copy our current monorepo
COPY . .
RUN npm ci --production
# Install dependencies only when needed
FROM base AS builder
WORKDIR /app
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
# Copy our current monorepo
COPY . .
RUN npm ci
RUN npm run build --workspaces
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=production_deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=production_deps --chown=nextjs:nodejs /app/package-lock.json ./package-lock.json
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/package.json ./package.json
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next ./.next
EXPOSE 3000
ENV PORT 3000
CMD ["npm", "run", "start"]

28
docker/build.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
command -v docker >/dev/null 2>&1 || {
echo "Docker is not running. Please start Docker and try again."
exit 1
}
command -v jq >/dev/null 2>&1 || {
echo "jq is not installed. Please install jq and try again."
exit 1
}
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
APP_VERSION="$(jq -r '.version' "$MONOREPO_ROOT/apps/web/package.json")"
GIT_SHA="$(git rev-parse HEAD)"
echo "Building docker image for monorepo at $MONOREPO_ROOT"
echo "App version: $APP_VERSION"
echo "Git SHA: $GIT_SHA"
docker build -f "$SCRIPT_DIR/Dockerfile" \
--progress=plain \
-t "documentso:latest" \
-t "documenso:$GIT_SHA" \
-t "documenso:$APP_VERSION" \
"$MONOREPO_ROOT"

12
docker/compose-entrypoint.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
cd "$MONOREPO_ROOT"
npm ci
npm run db-migrate:dev
npm run dev

View File

@@ -0,0 +1,16 @@
services:
database:
image: postgres:15
environment:
- POSTGRES_USER=documenso
- POSTGRES_PASSWORD=password
- POSTGRES_DB=documenso
ports:
- 54320:5432
inbucket:
image: inbucket/inbucket
ports:
- 9000:9000
- 2500:2500
- 1100:1100

40
docker/compose.yml Normal file
View File

@@ -0,0 +1,40 @@
services:
database:
image: postgres:15
environment:
- POSTGRES_USER=documenso
- POSTGRES_PASSWORD=password
- POSTGRES_DB=documenso
ports:
- 5432:5432
inbucket:
image: inbucket/inbucket
ports:
- 9000:9000
- 2500:2500
- 1100:1100
documenso:
image: node:18
working_dir: /app
command: ./docker/compose-entrypoint.sh
depends_on:
- database
- inbucket
environment:
- DATABASE_URL=postgres://documenso:password@database:5432/documenso
- NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
- NEXTAUTH_SECRET=my-super-secure-secret
- NEXTAUTH_URL=http://localhost:3000
- SENDGRID_API_KEY=
- SMTP_MAIL_HOST=inbucket
- SMTP_MAIL_PORT=2500
- SMTP_MAIL_USER=username
- SMTP_MAIL_PASSWORD=password
- MAIL_FROM=admin@example.com
- ALLOW_SIGNUP=true
ports:
- 3000:3000
volumes:
- ../:/app

1
documenso Submodule

Submodule documenso added at 0dcab27e65

1443
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,17 @@
"name": "documenso-monorepo",
"version": "0.0.0",
"scripts": {
"dev": "cd apps && cd web && next dev",
"dev": "npm run dev -w apps/web",
"build": "npm i && cd apps && cd web && npm i && next build",
"start": "cd apps && cd web && next start",
"db-migrate:dev": "prisma migrate dev",
"db-seed": "prisma db seed",
"db-studio": "prisma studio"
"db-studio": "prisma studio",
"docker:compose": "docker-compose -f ./docker/compose-without-app.yml",
"docker:compose-up": "npm run docker:compose -- up -d",
"docker:compose-down": "npm run docker:compose -- down",
"dx": "npm install && run-s docker:compose-up db-migrate:dev",
"d": "npm install && run-s docker:compose-up db-migrate:dev && npm run db-seed && npm run dev"
},
"workspaces": [
"apps/*",
@@ -43,5 +48,11 @@
"react-hot-toast": "^2.4.0",
"react-signature-canvas": "^1.0.6",
"typescript": "4.8.4"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.5"
}
}

View File

@@ -14,10 +14,8 @@ export const createField = (
if (newFieldX < 0) newFieldX = 0;
if (newFieldY < 0) newFieldY = 0;
if (newFieldX + fieldSize.width > rect.width)
newFieldX = rect.width - fieldSize.width;
if (newFieldY + fieldSize.height > rect.height)
newFieldY = rect.height - fieldSize.height;
if (newFieldX + fieldSize.width > rect.width) newFieldX = rect.width - fieldSize.width;
if (newFieldY + fieldSize.height > rect.height) newFieldY = rect.height - fieldSize.height;
const signatureField = {
id: -1,

View File

@@ -1,6 +1,6 @@
import router from "next/router";
import toast from "react-hot-toast";
import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants";
import toast from "react-hot-toast";
export const uploadDocument = async (event: any) => {
if (event.target.files && event.target.files[0]) {

View File

@@ -6,16 +6,13 @@ export const deleteRecipient = (recipient: any) => {
}
return toast.promise(
fetch(
"/api/documents/" + recipient.documentId + "/recipients/" + recipient.id,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(recipient),
}
),
fetch("/api/documents/" + recipient.documentId + "/recipients/" + recipient.id, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(recipient),
}),
{
loading: "Deleting...",
success: "Deleted.",

View File

@@ -7,4 +7,4 @@ export { getDocuments } from "./getDocuments";
export { deleteDocument } from "./deleteDocument";
export { deleteRecipient } from "./deleteRecipient";
export { createOrUpdateRecipient } from "./createOrUpdateRecipient";
export { sendSigningRequests } from "./sendSigningRequests";
export { sendSigningRequests } from "./sendSigningRequests";

View File

@@ -1,9 +1,6 @@
import toast from "react-hot-toast";
export const sendSigningRequests = async (
document: any,
resendTo: number[] = []
) => {
export const sendSigningRequests = async (document: any, resendTo: number[] = []) => {
if (!document || !document.id) return;
try {
const sent = await toast.promise(

View File

@@ -1,11 +1,7 @@
import { useRouter } from "next/router";
import toast from "react-hot-toast";
export const signDocument = (
document: any,
signatures: any[],
token: string
): Promise<any> => {
export const signDocument = (document: any, signatures: any[], token: string): Promise<any> => {
const body = { documentId: document.id, signatures };
return toast.promise(

View File

@@ -1,12 +1,8 @@
import { compare, hash } from "bcryptjs";
import type { NextApiRequest } from "next";
import type { Session } from "next-auth";
import {
getSession as getSessionInner,
GetSessionParams,
} from "next-auth/react";
import { HttpError } from "@documenso/lib/server";
import { compare, hash } from "bcryptjs";
import type { Session } from "next-auth";
import { GetSessionParams, getSession as getSessionInner } from "next-auth/react";
export async function hashPassword(password: string) {
const hashedPassword = await hash(password, 12);
@@ -28,9 +24,7 @@ export function validPassword(password: string) {
return true;
}
export async function getSession(
options: GetSessionParams
): Promise<Session | null> {
export async function getSession(options: GetSessionParams): Promise<Session | null> {
const session = await getSessionInner(options);
// that these are equal are ensured in `[...nextauth]`'s callback
@@ -43,11 +37,7 @@ export function isPasswordValid(
breakdown: boolean,
strict?: boolean
): { caplow: boolean; num: boolean; min: boolean; admin_min: boolean };
export function isPasswordValid(
password: string,
breakdown?: boolean,
strict?: boolean
) {
export function isPasswordValid(password: string, breakdown?: boolean, strict?: boolean) {
let cap = false, // Has uppercase characters
low = false, // Has lowercase characters
num = false, // At least one number
@@ -63,8 +53,7 @@ export function isPasswordValid(
}
}
if (!breakdown)
return cap && low && num && min && (strict ? admin_min : true);
if (!breakdown) return cap && low && num && min && (strict ? admin_min : true);
let errors: Record<string, boolean> = { caplow: cap && low, num, min };
// Only return the admin key if strict mode is enabled.
@@ -79,8 +68,7 @@ type CtxOrReq =
export const ensureSession = async (ctxOrReq: CtxOrReq) => {
const session = await getSession(ctxOrReq);
if (!session?.user)
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
if (!session?.user) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
return session;
};

View File

@@ -1,13 +1,9 @@
import { sendMail } from "./sendMail";
import { signingCompleteTemplate } from "@documenso/lib/mail";
import { Document as PrismaDocument } from "@prisma/client";
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
import { sendMail } from "./sendMail";
import { Document as PrismaDocument } from "@prisma/client";
export const sendSigningDoneMail = async (
recipient: any,
document: PrismaDocument,
user: any
) => {
export const sendSigningDoneMail = async (document: PrismaDocument, user: any) => {
await sendMail(
user.email,
`Completed: "${document.title}"`,
@@ -15,10 +11,7 @@ export const sendSigningDoneMail = async (
[
{
filename: document.title,
content: Buffer.from(
await addDigitalSignature(document.document),
"base64"
),
content: Buffer.from(await addDigitalSignature(document.document), "base64"),
},
]
);

View File

@@ -1,8 +1,8 @@
import prisma from "@documenso/prisma";
import { sendMail } from "./sendMail";
import { SendStatus, ReadStatus, DocumentStatus } from "@prisma/client";
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { signingRequestTemplate } from "@documenso/lib/mail";
import prisma from "@documenso/prisma";
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { sendMail } from "./sendMail";
import { DocumentStatus, ReadStatus, SendStatus } from "@prisma/client";
export const sendSigningRequest = async (recipient: any, document: any, user: any) => {
const signingRequestMessage = user.name

View File

@@ -1,6 +1,6 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { Document as PrismaDocument } from "@prisma/client";
import { baseEmailTemplate } from "./baseTemplate";
import { Document as PrismaDocument } from "@prisma/client";
export const signingCompleteTemplate = (message: string) => {
const customContent = `

View File

@@ -1,6 +1,6 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { Document as PrismaDocument } from "@prisma/client";
import { baseEmailTemplate } from "./baseTemplate";
import { Document as PrismaDocument } from "@prisma/client";
export const signingRequestTemplate = (
message: string,

View File

@@ -1,9 +1,7 @@
import { getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
export const getDocumentsForUserFromToken = async (
context: any
): Promise<any> => {
export const getDocumentsForUserFromToken = async (context: any): Promise<any> => {
const user = await getUserFromToken(context.req, context.res);
if (!user) return Promise.reject("Invalid user or token.");

View File

@@ -1,24 +1,27 @@
import type { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
type Handlers = {
[method in "GET" | "POST" | "PATCH" | "PUT" | "DELETE"]?: Promise<{ default: NextApiHandler }>;
[method in "GET" | "POST" | "PATCH" | "PUT" | "DELETE"]?: Promise<{
default: NextApiHandler;
}>;
};
/** Allows us to split big API handlers by method */
export const defaultHandler = (handlers: Handlers) => async (req: NextApiRequest, res: NextApiResponse) => {
const handler = (await handlers[req.method as keyof typeof handlers])?.default;
// auto catch unsupported methods.
if (!handler) {
return res
.status(405)
.json({ message: `Method Not Allowed (Allow: ${Object.keys(handlers).join(",")})` });
}
export const defaultHandler =
(handlers: Handlers) => async (req: NextApiRequest, res: NextApiResponse) => {
const handler = (await handlers[req.method as keyof typeof handlers])?.default;
// auto catch unsupported methods.
if (!handler) {
return res.status(405).json({
message: `Method Not Allowed (Allow: ${Object.keys(handlers).join(",")})`,
});
}
try {
await handler(req, res);
return;
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Something went wrong" });
}
};
try {
await handler(req, res);
return;
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Something went wrong" });
}
};

View File

@@ -1,5 +1,4 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerErrorFromUnknown } from "@documenso/lib/server";
type Handle<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>;

View File

@@ -1,9 +1,5 @@
import {
PrismaClientKnownRequestError,
NotFoundError,
} from "@prisma/client/runtime";
import { HttpError } from "@documenso/lib/server";
import { NotFoundError, PrismaClientKnownRequestError } from "@prisma/client/runtime";
export function getServerErrorFromUnknown(cause: unknown): HttpError {
// Error was manually thrown and does not need to be parsed.

View File

@@ -1,6 +1,6 @@
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@documenso/prisma";
import { User as PrismaUser } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { signOut } from "next-auth/react";
@@ -26,7 +26,7 @@ export async function getUserFromToken(
});
if (!user) {
if (res) res.status(401).end();
if (res && res.status) res.status(401).end();
return null;
}

View File

@@ -5,7 +5,13 @@ export class HttpError<TCode extends number = number> extends Error {
public readonly url: string | undefined;
public readonly method: string | undefined;
constructor(opts: { url?: string; method?: string; message?: string; statusCode: TCode; cause?: Error }) {
constructor(opts: {
url?: string;
method?: string;
message?: string;
statusCode: TCode;
cause?: Error;
}) {
super(opts.message ?? `HTTP Error ${opts.statusCode} `);
Object.setPrototypeOf(this, HttpError.prototype);

View File

@@ -1,6 +1,6 @@
import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
import fontkit from "@pdf-lib/fontkit";
import * as fs from "fs";
import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
export async function insertTextInPDF(
pdfAsBase64: string,
@@ -12,27 +12,36 @@ export async function insertTextInPDF(
): Promise<string> {
const fontBytes = fs.readFileSync("public/fonts/Qwigley-Regular.ttf");
const existingPdfBytes = pdfAsBase64;
const pdfDoc = await PDFDocument.load(pdfAsBase64);
const pdfDoc = await PDFDocument.load(existingPdfBytes);
pdfDoc.registerFontkit(fontkit);
const customFont = await pdfDoc.embedFont(fontBytes);
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
const font = await pdfDoc.embedFont(useHandwritingFont ? fontBytes : StandardFonts.Helvetica);
const pages = pdfDoc.getPages();
const pdfPage = pages[page];
const textSize = useHandwritingFont ? 50 : 15;
const textWidth = customFont.widthOfTextAtSize(text, textSize);
const textHeight = customFont.heightAtSize(textSize);
const textWidth = font.widthOfTextAtSize(text, textSize);
const textHeight = font.heightAtSize(textSize);
const fieldSize = { width: 192, height: 64 };
const invertedYPosition = pdfPage.getHeight() - positionY - fieldSize.height;
// Because pdf-lib use a bottom-left coordinate system, we need to invert the y position
// we then center the text in the middle by adding half the height of the text
// plus the height of the field and divide the result by 2
const invertedYPosition =
pdfPage.getHeight() - positionY - (fieldSize.height + textHeight / 2) / 2;
// We center the text by adding the width of the field, subtracting the width of the text
// and dividing the result by 2
const centeredXPosition = positionX + (fieldSize.width - textWidth) / 2;
pdfPage.drawText(text, {
x: positionX,
x: centeredXPosition,
y: invertedYPosition,
size: textSize,
font: useHandwritingFont ? customFont : helveticaFont,
color: rgb(0, 0, 0),
font,
});
const pdfAsUint8Array = await pdfDoc.save();

View File

@@ -1,5 +1,5 @@
import { PrismaClient, Document, User } from "@prisma/client";
import { isENVProd } from "@documenso/lib"
import { isENVProd } from "@documenso/lib";
import { Document, PrismaClient, User } from "@prisma/client";
declare global {
var client: PrismaClient | undefined;

View File

@@ -0,0 +1,5 @@
-- DropForeignKey
ALTER TABLE "Signature" DROP CONSTRAINT "Signature_recipientId_fkey";
-- AddForeignKey
ALTER TABLE "Signature" ADD CONSTRAINT "Signature_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -130,6 +130,6 @@ model Signature {
signatureImageAsBase64 String?
typedSignature String?
Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Restrict)
Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
Field Field @relation(fields: [fieldId], references: [id], onDelete: Restrict)
}

View File

@@ -1,7 +1,7 @@
import prisma from "@documenso/prisma";
import { hashPassword } from "@documenso/lib/auth";
import { IdentityProvider } from "@prisma/client";
import { coloredConsole } from "@documenso/lib";
import { hashPassword } from "@documenso/lib/auth";
import prisma from "@documenso/prisma";
import { IdentityProvider } from "@prisma/client";
async function createUser(userData: { email: string; password: string }) {
try {
@@ -24,7 +24,7 @@ async function createUser(userData: { email: string; password: string }) {
async function main() {
console.info("Start seeding...");
const password = "123456789";
const email = "example6@documenso.com";
const email = "example@documenso.com";
const user = await createUser({
email: email,
password: await hashPassword(password),

View File

@@ -1,17 +1,10 @@
import { PDFDocument, PDFHexString, PDFName, PDFNumber, PDFString } from "pdf-lib";
const fs = require("fs");
// Local copy of Node SignPDF because https://github.com/vbuch/node-signpdf/pull/187 was not published in NPM yet. Can be switched to npm packge.
const signer = require("./node-signpdf/dist/signpdf");
import {
PDFDocument,
PDFName,
PDFNumber,
PDFHexString,
PDFString,
} from "pdf-lib";
export const addDigitalSignature = async (
documentAsBase64: string
): Promise<string> => {
export const addDigitalSignature = async (documentAsBase64: string): Promise<string> => {
// Custom code to add Byterange to PDF
const PDFArrayCustom = require("./PDFArrayCustom");
const pdfBuffer = Buffer.from(documentAsBase64, "base64");

View File

@@ -1 +1 @@
export { signDocument } from "./signDocument";
export { signDocument } from "./signDocument";

View File

@@ -1,9 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = exports.ERROR_VERIFY_SIGNATURE = exports.ERROR_TYPE_UNKNOWN = exports.ERROR_TYPE_PARSE = exports.ERROR_TYPE_INPUT = void 0;
exports.default =
exports.ERROR_VERIFY_SIGNATURE =
exports.ERROR_TYPE_UNKNOWN =
exports.ERROR_TYPE_PARSE =
exports.ERROR_TYPE_INPUT =
void 0;
const ERROR_TYPE_UNKNOWN = 1;
exports.ERROR_TYPE_UNKNOWN = ERROR_TYPE_UNKNOWN;
const ERROR_TYPE_INPUT = 2;
@@ -18,13 +23,11 @@ class SignPdfError extends Error {
super(msg);
this.type = type;
}
} // Shorthand
SignPdfError.TYPE_UNKNOWN = ERROR_TYPE_UNKNOWN;
SignPdfError.TYPE_INPUT = ERROR_TYPE_INPUT;
SignPdfError.TYPE_PARSE = ERROR_TYPE_PARSE;
SignPdfError.VERIFY_SIGNATURE = ERROR_VERIFY_SIGNATURE;
var _default = SignPdfError;
exports.default = _default;
exports.default = _default;

View File

@@ -1,18 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.SUBFILTER_ETSI_CADES_DETACHED = exports.SUBFILTER_ADOBE_X509_SHA1 = exports.SUBFILTER_ADOBE_PKCS7_SHA1 = exports.SUBFILTER_ADOBE_PKCS7_DETACHED = exports.DEFAULT_SIGNATURE_LENGTH = exports.DEFAULT_BYTE_RANGE_PLACEHOLDER = void 0;
exports.SUBFILTER_ETSI_CADES_DETACHED =
exports.SUBFILTER_ADOBE_X509_SHA1 =
exports.SUBFILTER_ADOBE_PKCS7_SHA1 =
exports.SUBFILTER_ADOBE_PKCS7_DETACHED =
exports.DEFAULT_SIGNATURE_LENGTH =
exports.DEFAULT_BYTE_RANGE_PLACEHOLDER =
void 0;
const DEFAULT_SIGNATURE_LENGTH = 8192;
exports.DEFAULT_SIGNATURE_LENGTH = DEFAULT_SIGNATURE_LENGTH;
const DEFAULT_BYTE_RANGE_PLACEHOLDER = '**********';
const DEFAULT_BYTE_RANGE_PLACEHOLDER = "**********";
exports.DEFAULT_BYTE_RANGE_PLACEHOLDER = DEFAULT_BYTE_RANGE_PLACEHOLDER;
const SUBFILTER_ADOBE_PKCS7_DETACHED = 'adbe.pkcs7.detached';
const SUBFILTER_ADOBE_PKCS7_DETACHED = "adbe.pkcs7.detached";
exports.SUBFILTER_ADOBE_PKCS7_DETACHED = SUBFILTER_ADOBE_PKCS7_DETACHED;
const SUBFILTER_ADOBE_PKCS7_SHA1 = 'adbe.pkcs7.sha1';
const SUBFILTER_ADOBE_PKCS7_SHA1 = "adbe.pkcs7.sha1";
exports.SUBFILTER_ADOBE_PKCS7_SHA1 = SUBFILTER_ADOBE_PKCS7_SHA1;
const SUBFILTER_ADOBE_X509_SHA1 = 'adbe.x509.rsa.sha1';
const SUBFILTER_ADOBE_X509_SHA1 = "adbe.x509.rsa.sha1";
exports.SUBFILTER_ADOBE_X509_SHA1 = SUBFILTER_ADOBE_X509_SHA1;
const SUBFILTER_ETSI_CADES_DETACHED = 'ETSI.CAdES.detached';
exports.SUBFILTER_ETSI_CADES_DETACHED = SUBFILTER_ETSI_CADES_DETACHED;
const SUBFILTER_ETSI_CADES_DETACHED = "ETSI.CAdES.detached";
exports.SUBFILTER_ETSI_CADES_DETACHED = SUBFILTER_ETSI_CADES_DETACHED;

View File

@@ -1,13 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
var _SignPdfError = _interopRequireDefault(require("../SignPdfError"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const getSubstringIndex = (str, substring, n) => {
let times = 0;
@@ -30,42 +32,55 @@ const getSubstringIndex = (str, substring, n) => {
* @returns {Object} {ByteRange: Number[], signature: Buffer, signedData: Buffer}
*/
const extractSignature = (pdf, signatureCount = 1) => {
if (!(pdf instanceof Buffer)) {
throw new _SignPdfError.default('PDF expected as Buffer.', _SignPdfError.default.TYPE_INPUT);
throw new _SignPdfError.default("PDF expected as Buffer.", _SignPdfError.default.TYPE_INPUT);
} // const byteRangePos = pdf.indexOf('/ByteRange [');
const byteRangePos = getSubstringIndex(pdf, '/ByteRange [', signatureCount);
const byteRangePos = getSubstringIndex(pdf, "/ByteRange [", signatureCount);
if (byteRangePos === -1) {
throw new _SignPdfError.default('Failed to locate ByteRange.', _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
"Failed to locate ByteRange.",
_SignPdfError.default.TYPE_PARSE
);
}
const byteRangeEnd = pdf.indexOf(']', byteRangePos);
const byteRangeEnd = pdf.indexOf("]", byteRangePos);
if (byteRangeEnd === -1) {
throw new _SignPdfError.default('Failed to locate the end of the ByteRange.', _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
"Failed to locate the end of the ByteRange.",
_SignPdfError.default.TYPE_PARSE
);
}
const byteRange = pdf.slice(byteRangePos, byteRangeEnd + 1).toString();
const matches = /\/ByteRange \[(\d+) +(\d+) +(\d+) +(\d+) *\]/.exec(byteRange);
if (matches === null) {
throw new _SignPdfError.default('Failed to parse the ByteRange.', _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
"Failed to parse the ByteRange.",
_SignPdfError.default.TYPE_PARSE
);
}
const ByteRange = matches.slice(1).map(Number);
const signedData = Buffer.concat([pdf.slice(ByteRange[0], ByteRange[0] + ByteRange[1]), pdf.slice(ByteRange[2], ByteRange[2] + ByteRange[3])]);
const signatureHex = pdf.slice(ByteRange[0] + ByteRange[1] + 1, ByteRange[2]).toString('binary').replace(/(?:00|>)+$/, '');
const signature = Buffer.from(signatureHex, 'hex').toString('binary');
const signedData = Buffer.concat([
pdf.slice(ByteRange[0], ByteRange[0] + ByteRange[1]),
pdf.slice(ByteRange[2], ByteRange[2] + ByteRange[3]),
]);
const signatureHex = pdf
.slice(ByteRange[0] + ByteRange[1] + 1, ByteRange[2])
.toString("binary")
.replace(/(?:00|>)+$/, "");
const signature = Buffer.from(signatureHex, "hex").toString("binary");
return {
ByteRange: matches.slice(1, 5).map(Number),
signature,
signedData
signedData,
};
};
var _default = extractSignature;
exports.default = _default;
exports.default = _default;

View File

@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
@@ -9,7 +9,9 @@ var _SignPdfError = _interopRequireDefault(require("../SignPdfError"));
var _const = require("./const");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
/**
* Finds ByteRange information within a given PDF Buffer if one exists
@@ -17,25 +19,32 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
* @param {Buffer} pdf
* @returns {Object} {byteRangePlaceholder: String, byteRangeStrings: String[], byteRange: String[]}
*/
const findByteRange = pdf => {
const findByteRange = (pdf) => {
if (!(pdf instanceof Buffer)) {
throw new _SignPdfError.default('PDF expected as Buffer.', _SignPdfError.default.TYPE_INPUT);
throw new _SignPdfError.default("PDF expected as Buffer.", _SignPdfError.default.TYPE_INPUT);
}
const byteRangeStrings = pdf.toString().match(/\/ByteRange\s*\[{1}\s*(?:(?:\d*|\/\*{10})\s+){3}(?:\d+|\/\*{10}){1}\s*]{1}/g);
const byteRangeStrings = pdf
.toString()
.match(/\/ByteRange\s*\[{1}\s*(?:(?:\d*|\/\*{10})\s+){3}(?:\d+|\/\*{10}){1}\s*]{1}/g);
if (!byteRangeStrings) {
throw new _SignPdfError.default('No ByteRangeStrings found within PDF buffer', _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
"No ByteRangeStrings found within PDF buffer",
_SignPdfError.default.TYPE_PARSE
);
}
const byteRangePlaceholder = byteRangeStrings.find(s => s.includes(`/${_const.DEFAULT_BYTE_RANGE_PLACEHOLDER}`));
const byteRanges = byteRangeStrings.map(brs => brs.match(/[^[\s]*(?:\d|\/\*{10})/g));
const byteRangePlaceholder = byteRangeStrings.find((s) =>
s.includes(`/${_const.DEFAULT_BYTE_RANGE_PLACEHOLDER}`)
);
const byteRanges = byteRangeStrings.map((brs) => brs.match(/[^[\s]*(?:\d|\/\*{10})/g));
return {
byteRangePlaceholder,
byteRangeStrings,
byteRanges
byteRanges,
};
};
var _default = findByteRange;
exports.default = _default;
exports.default = _default;

View File

@@ -1,37 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
Object.defineProperty(exports, "extractSignature", {
enumerable: true,
get: function () {
return _extractSignature.default;
}
},
});
Object.defineProperty(exports, "findByteRange", {
enumerable: true,
get: function () {
return _findByteRange.default;
}
},
});
Object.defineProperty(exports, "pdfkitAddPlaceholder", {
enumerable: true,
get: function () {
return _pdfkitAddPlaceholder.default;
}
},
});
Object.defineProperty(exports, "plainAddPlaceholder", {
enumerable: true,
get: function () {
return _plainAddPlaceholder.default;
}
},
});
Object.defineProperty(exports, "removeTrailingNewLine", {
enumerable: true,
get: function () {
return _removeTrailingNewLine.default;
}
},
});
var _extractSignature = _interopRequireDefault(require("./extractSignature"));
@@ -44,6 +44,8 @@ var _removeTrailingNewLine = _interopRequireDefault(require("./removeTrailingNew
var _findByteRange = _interopRequireDefault(require("./findByteRange"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
'This string is added so that jest collects coverage for this file'; // eslint-disable-line
("This string is added so that jest collects coverage for this file"); // eslint-disable-line

View File

@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
@@ -17,10 +17,9 @@ PDFAbstractReference - abstract class for PDF reference
*/
class PDFAbstractReference {
toString() {
throw new Error('Must be implemented by subclasses');
throw new Error("Must be implemented by subclasses");
}
}
var _default = PDFAbstractReference;
exports.default = _default;
exports.default = _default;

View File

@@ -1,13 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
var _abstract_reference = _interopRequireDefault(require("./abstract_reference"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
/*
PDFObject by Devon Govett used below.
@@ -20,26 +22,26 @@ Modifications may have been applied for the purposes of node-signpdf.
PDFObject - converts JavaScript types into their corresponding PDF types.
By Devon Govett
*/
const pad = (str, length) => (Array(length + 1).join('0') + str).slice(-length);
const pad = (str, length) => (Array(length + 1).join("0") + str).slice(-length);
const escapableRe = /[\n\r\t\b\f()\\]/g;
const escapable = {
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\b': '\\b',
'\f': '\\f',
'\\': '\\\\',
'(': '\\(',
')': '\\)'
"\n": "\\n",
"\r": "\\r",
"\t": "\\t",
"\b": "\\b",
"\f": "\\f",
"\\": "\\\\",
"(": "\\(",
")": "\\)",
}; // Convert little endian UTF-16 to big endian
const swapBytes = buff => buff.swap16();
const swapBytes = (buff) => buff.swap16();
class PDFObject {
static convert(object, encryptFn = null) {
// String literals are converted to the PDF name type
if (typeof object === 'string') {
if (typeof object === "string") {
return `/${object}`; // String objects are converted to PDF strings (UTF-16)
}
@@ -55,29 +57,26 @@ class PDFObject {
}
} // If so, encode it as big endian UTF-16
let stringBuffer;
if (isUnicode) {
stringBuffer = swapBytes(Buffer.from(`\ufeff${string}`, 'utf16le'));
stringBuffer = swapBytes(Buffer.from(`\ufeff${string}`, "utf16le"));
} else {
stringBuffer = Buffer.from(string, 'ascii');
stringBuffer = Buffer.from(string, "ascii");
} // Encrypt the string when necessary
if (encryptFn) {
string = encryptFn(stringBuffer).toString('binary');
string = encryptFn(stringBuffer).toString("binary");
} else {
string = stringBuffer.toString('binary');
string = stringBuffer.toString("binary");
} // Escape characters as required by the spec
string = string.replace(escapableRe, c => escapable[c]);
string = string.replace(escapableRe, (c) => escapable[c]);
return `(${string})`; // Buffers are converted to PDF hex strings
}
if (Buffer.isBuffer(object)) {
return `<${object.toString('hex')}>`;
return `<${object.toString("hex")}>`;
}
if (object instanceof _abstract_reference.default) {
@@ -85,51 +84,57 @@ class PDFObject {
}
if (object instanceof Date) {
let string = `D:${pad(object.getUTCFullYear(), 4)}${pad(object.getUTCMonth() + 1, 2)}${pad(object.getUTCDate(), 2)}${pad(object.getUTCHours(), 2)}${pad(object.getUTCMinutes(), 2)}${pad(object.getUTCSeconds(), 2)}Z`; // Encrypt the string when necessary
let string = `D:${pad(object.getUTCFullYear(), 4)}${pad(object.getUTCMonth() + 1, 2)}${pad(
object.getUTCDate(),
2
)}${pad(object.getUTCHours(), 2)}${pad(object.getUTCMinutes(), 2)}${pad(
object.getUTCSeconds(),
2
)}Z`; // Encrypt the string when necessary
if (encryptFn) {
string = encryptFn(Buffer.from(string, 'ascii')).toString('binary'); // Escape characters as required by the spec
string = encryptFn(Buffer.from(string, "ascii")).toString("binary"); // Escape characters as required by the spec
string = string.replace(escapableRe, c => escapable[c]);
string = string.replace(escapableRe, (c) => escapable[c]);
}
return `(${string})`;
}
if (Array.isArray(object)) {
const items = object.map(e => PDFObject.convert(e, encryptFn)).join(' ');
const items = object.map((e) => PDFObject.convert(e, encryptFn)).join(" ");
return `[${items}]`;
}
if ({}.toString.call(object) === '[object Object]') {
const out = ['<<'];
if ({}.toString.call(object) === "[object Object]") {
const out = ["<<"];
let streamData; // @todo this can probably be refactored into a reduce
Object.entries(object).forEach(([key, val]) => {
let checkedValue = '';
let checkedValue = "";
if (val.toString().indexOf('<<') !== -1) {
if (val.toString().indexOf("<<") !== -1) {
checkedValue = val;
} else {
checkedValue = PDFObject.convert(val, encryptFn);
}
if (key === 'stream') {
if (key === "stream") {
streamData = `${key}\n${val}\nendstream`;
} else {
out.push(`/${key} ${checkedValue}`);
}
});
out.push('>>');
out.push(">>");
if (streamData) {
out.push(streamData);
}
return out.join('\n');
return out.join("\n");
}
if (typeof object === 'number') {
if (typeof object === "number") {
return PDFObject.number(object);
}
@@ -143,7 +148,6 @@ class PDFObject {
throw new Error(`unsupported number: ${n}`);
}
}
exports.default = PDFObject;
exports.default = PDFObject;

View File

@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
@@ -9,7 +9,9 @@ var _const = require("./const");
var _pdfkitReferenceMock = _interopRequireDefault(require("./pdfkitReferenceMock"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
// eslint-disable-next-line import/no-unresolved
@@ -25,18 +27,18 @@ const pdfkitAddPlaceholder = ({
pdf,
pdfBuffer,
reason,
contactInfo = 'emailfromp1289@gmail.com',
name = 'Name from p12',
location = 'Location from p12',
contactInfo = "emailfromp1289@gmail.com",
name = "Name from p12",
location = "Location from p12",
signatureLength = _const.DEFAULT_SIGNATURE_LENGTH,
byteRangePlaceholder = _const.DEFAULT_BYTE_RANGE_PLACEHOLDER,
subFilter = _const.SUBFILTER_ADOBE_PKCS7_DETACHED
subFilter = _const.SUBFILTER_ADOBE_PKCS7_DETACHED,
}) => {
/* eslint-disable no-underscore-dangle,no-param-reassign */
// Generate the signature placeholder
const signature = pdf.ref({
Type: 'Sig',
Filter: 'Adobe.PPKLite',
Type: "Sig",
Filter: "Adobe.PPKLite",
SubFilter: subFilter,
ByteRange: [0, byteRangePlaceholder, byteRangePlaceholder, byteRangePlaceholder],
Contents: Buffer.from(String.fromCharCode(0).repeat(signatureLength)),
@@ -47,11 +49,10 @@ const pdfkitAddPlaceholder = ({
// eslint-disable-line no-new-wrappers
Name: new String(name),
// eslint-disable-line no-new-wrappers
Location: new String(location) // eslint-disable-line no-new-wrappers
Location: new String(location), // eslint-disable-line no-new-wrappers
}); // Check if pdf already contains acroform field
const acroFormPosition = pdfBuffer.lastIndexOf('/Type /AcroForm');
const acroFormPosition = pdfBuffer.lastIndexOf("/Type /AcroForm");
const isAcroFormExists = acroFormPosition !== -1;
let fieldIds = [];
let acroFormId;
@@ -65,13 +66,13 @@ const pdfkitAddPlaceholder = ({
// (generally it's 2 or 3, but I'm giving a big space though)
const maxAcroFormIdLength = 12;
let foundAcroFormId = '';
let foundAcroFormId = "";
let index = charsUntilIdEnd + 1;
for (index; index < charsUntilIdEnd + maxAcroFormIdLength; index += 1) {
const acroFormIdString = pdfBuffer.slice(acroFormPosition - index, acroFormIdEnd).toString();
if (acroFormIdString[0] === '\n') {
if (acroFormIdString[0] === "\n") {
break;
}
@@ -80,25 +81,27 @@ const pdfkitAddPlaceholder = ({
}
const pdfSlice = pdfBuffer.slice(acroFormStart);
const acroForm = pdfSlice.slice(0, pdfSlice.indexOf('endobj')).toString();
const acroForm = pdfSlice.slice(0, pdfSlice.indexOf("endobj")).toString();
acroFormId = parseInt(foundAcroFormId);
const acroFormFields = acroForm.slice(acroForm.indexOf('/Fields [') + 9, acroForm.indexOf(']'));
fieldIds = acroFormFields.split(' ').filter((element, i) => i % 3 === 0).map(fieldId => new _pdfkitReferenceMock.default(fieldId));
const acroFormFields = acroForm.slice(acroForm.indexOf("/Fields [") + 9, acroForm.indexOf("]"));
fieldIds = acroFormFields
.split(" ")
.filter((element, i) => i % 3 === 0)
.map((fieldId) => new _pdfkitReferenceMock.default(fieldId));
}
const signatureName = 'Signature'; // Generate signature annotation widget
const signatureName = "Signature"; // Generate signature annotation widget
const widget = pdf.ref({
Type: 'Annot',
Subtype: 'Widget',
FT: 'Sig',
Type: "Annot",
Subtype: "Widget",
FT: "Sig",
Rect: [0, 0, 0, 0],
V: signature,
T: new String(signatureName + (fieldIds.length + 1)),
// eslint-disable-line no-new-wrappers
F: 4,
P: pdf.page.dictionary // eslint-disable-line no-underscore-dangle
P: pdf.page.dictionary, // eslint-disable-line no-underscore-dangle
});
pdf.page.dictionary.data.Annots = [widget]; // Include the widget in a page
@@ -107,27 +110,30 @@ const pdfkitAddPlaceholder = ({
if (!isAcroFormExists) {
// Create a form (with the widget) and link in the _root
form = pdf.ref({
Type: 'AcroForm',
Type: "AcroForm",
SigFlags: 3,
Fields: [...fieldIds, widget]
Fields: [...fieldIds, widget],
});
} else {
// Use existing acroform and extend the fields with newly created widgets
form = pdf.ref({
Type: 'AcroForm',
SigFlags: 3,
Fields: [...fieldIds, widget]
}, acroFormId);
form = pdf.ref(
{
Type: "AcroForm",
SigFlags: 3,
Fields: [...fieldIds, widget],
},
acroFormId
);
}
pdf._root.data.AcroForm = form;
return {
signature,
form,
widget
widget,
};
/* eslint-enable no-underscore-dangle,no-param-reassign */
};
var _default = pdfkitAddPlaceholder;
exports.default = _default;
exports.default = _default;

View File

@@ -1,20 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
var _abstract_reference = _interopRequireDefault(require("./pdfkit/abstract_reference"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
class PDFKitReferenceMock extends _abstract_reference.default {
constructor(index, additionalData = undefined) {
super();
this.index = index;
if (typeof additionalData !== 'undefined') {
if (typeof additionalData !== "undefined") {
Object.assign(this, additionalData);
}
}
@@ -22,8 +24,7 @@ class PDFKitReferenceMock extends _abstract_reference.default {
toString() {
return `${this.index} 0 R`;
}
}
var _default = PDFKitReferenceMock;
exports.default = _default;
exports.default = _default;

View File

@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
@@ -9,7 +9,9 @@ var _findObject = _interopRequireDefault(require("./findObject"));
var _getIndexFromRef = _interopRequireDefault(require("./getIndexFromRef"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const createBufferPageWithAnnotation = (pdf, info, pagesRef, widget) => {
const pagesDictionary = (0, _findObject.default)(pdf, info.xref, pagesRef).toString(); // Extend page dictionary with newly created annotations
@@ -17,16 +19,16 @@ const createBufferPageWithAnnotation = (pdf, info, pagesRef, widget) => {
let annotsStart;
let annotsEnd;
let annots;
annotsStart = pagesDictionary.indexOf('/Annots');
annotsStart = pagesDictionary.indexOf("/Annots");
if (annotsStart > -1) {
annotsEnd = pagesDictionary.indexOf(']', annotsStart);
annotsEnd = pagesDictionary.indexOf("]", annotsStart);
annots = pagesDictionary.substr(annotsStart, annotsEnd + 1 - annotsStart);
annots = annots.substr(0, annots.length - 1); // remove the trailing ]
} else {
annotsStart = pagesDictionary.length;
annotsEnd = pagesDictionary.length;
annots = '/Annots [';
annots = "/Annots [";
}
const pagesDictionaryIndex = (0, _getIndexFromRef.default)(info.xref, pagesRef);
@@ -34,14 +36,19 @@ const createBufferPageWithAnnotation = (pdf, info, pagesRef, widget) => {
annots = `${annots} ${widgetValue}]`; // add the trailing ] back
const preAnnots = pagesDictionary.substr(0, annotsStart);
let postAnnots = '';
let postAnnots = "";
if (pagesDictionary.length > annotsEnd) {
postAnnots = pagesDictionary.substr(annotsEnd + 1);
}
return Buffer.concat([Buffer.from(`${pagesDictionaryIndex} 0 obj\n`), Buffer.from('<<\n'), Buffer.from(`${preAnnots + annots + postAnnots}\n`), Buffer.from('\n>>\nendobj\n')]);
return Buffer.concat([
Buffer.from(`${pagesDictionaryIndex} 0 obj\n`),
Buffer.from("<<\n"),
Buffer.from(`${preAnnots + annots + postAnnots}\n`),
Buffer.from("\n>>\nendobj\n"),
]);
};
var _default = createBufferPageWithAnnotation;
exports.default = _default;
exports.default = _default;

View File

@@ -1,18 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
var _getIndexFromRef = _interopRequireDefault(require("./getIndexFromRef"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const createBufferRootWithAcroform = (pdf, info, form) => {
const rootIndex = (0, _getIndexFromRef.default)(info.xref, info.rootRef);
return Buffer.concat([Buffer.from(`${rootIndex} 0 obj\n`), Buffer.from('<<\n'), Buffer.from(`${info.root}\n`), Buffer.from(`/AcroForm ${form}`), Buffer.from('\n>>\nendobj\n')]);
return Buffer.concat([
Buffer.from(`${rootIndex} 0 obj\n`),
Buffer.from("<<\n"),
Buffer.from(`${info.root}\n`),
Buffer.from(`/AcroForm ${form}`),
Buffer.from("\n>>\nendobj\n"),
]);
};
var _default = createBufferRootWithAcroform;
exports.default = _default;
exports.default = _default;

View File

@@ -1,21 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
const createBufferTrailer = (pdf, info, addedReferences) => {
let rows = [];
rows[0] = '0000000000 65535 f '; // info.xref.tableRows[0];
rows[0] = "0000000000 65535 f "; // info.xref.tableRows[0];
addedReferences.forEach((offset, index) => {
const paddedOffset = `0000000000${offset}`.slice(-10);
rows[index + 1] = `${index} 1\n${paddedOffset} 00000 n `;
});
rows = rows.filter(row => row !== undefined);
return Buffer.concat([Buffer.from('xref\n'), Buffer.from(`${info.xref.startingIndex} 1\n`), Buffer.from(rows.join('\n')), Buffer.from('\ntrailer\n'), Buffer.from('<<\n'), Buffer.from(`/Size ${info.xref.maxIndex + 1}\n`), Buffer.from(`/Root ${info.rootRef}\n`), Buffer.from(info.infoRef ? `/Info ${info.infoRef}\n` : ''), Buffer.from(`/Prev ${info.xRefPosition}\n`), Buffer.from('>>\n'), Buffer.from('startxref\n'), Buffer.from(`${pdf.length}\n`), Buffer.from('%%EOF')]);
rows = rows.filter((row) => row !== undefined);
return Buffer.concat([
Buffer.from("xref\n"),
Buffer.from(`${info.xref.startingIndex} 1\n`),
Buffer.from(rows.join("\n")),
Buffer.from("\ntrailer\n"),
Buffer.from("<<\n"),
Buffer.from(`/Size ${info.xref.maxIndex + 1}\n`),
Buffer.from(`/Root ${info.rootRef}\n`),
Buffer.from(info.infoRef ? `/Info ${info.infoRef}\n` : ""),
Buffer.from(`/Prev ${info.xRefPosition}\n`),
Buffer.from(">>\n"),
Buffer.from("startxref\n"),
Buffer.from(`${pdf.length}\n`),
Buffer.from("%%EOF"),
]);
};
var _default = createBufferTrailer;
exports.default = _default;
exports.default = _default;

View File

@@ -1,13 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
var _getIndexFromRef = _interopRequireDefault(require("./getIndexFromRef"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
/**
* @param {Buffer} pdf
@@ -18,12 +20,12 @@ const findObject = (pdf, refTable, ref) => {
const index = (0, _getIndexFromRef.default)(refTable, ref);
const offset = refTable.offsets.get(index);
let slice = pdf.slice(offset);
slice = slice.slice(0, slice.indexOf('endobj', 'utf8')); // FIXME: What if it is a stream?
slice = slice.slice(0, slice.indexOf("endobj", "utf8")); // FIXME: What if it is a stream?
slice = slice.slice(slice.indexOf('<<', 'utf8') + 2);
slice = slice.slice(0, slice.lastIndexOf('>>', 'utf8'));
slice = slice.slice(slice.indexOf("<<", "utf8") + 2);
slice = slice.slice(0, slice.lastIndexOf(">>", "utf8"));
return slice;
};
var _default = findObject;
exports.default = _default;
exports.default = _default;

View File

@@ -1,13 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
var _SignPdfError = _interopRequireDefault(require("../../SignPdfError"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
/**
* @param {object} refTable
@@ -15,15 +17,18 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
* @returns {number}
*/
const getIndexFromRef = (refTable, ref) => {
let [index] = ref.split(' ');
let [index] = ref.split(" ");
index = parseInt(index);
if (!refTable.offsets.has(index)) {
throw new _SignPdfError.default(`Failed to locate object "${ref}".`, _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
`Failed to locate object "${ref}".`,
_SignPdfError.default.TYPE_PARSE
);
}
return index;
};
var _default = getIndexFromRef;
exports.default = _default;
exports.default = _default;

View File

@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = getPageRef;
@@ -9,7 +9,9 @@ var _getPagesDictionaryRef = _interopRequireDefault(require("./getPagesDictionar
var _findObject = _interopRequireDefault(require("./findObject"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
/**
* Finds the reference to a page.
@@ -20,10 +22,10 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
function getPageRef(pdfBuffer, info) {
const pagesRef = (0, _getPagesDictionaryRef.default)(info);
const pagesDictionary = (0, _findObject.default)(pdfBuffer, info.xref, pagesRef);
const kidsPosition = pagesDictionary.indexOf('/Kids');
const kidsStart = pagesDictionary.indexOf('[', kidsPosition) + 1;
const kidsEnd = pagesDictionary.indexOf(']', kidsPosition);
const kidsPosition = pagesDictionary.indexOf("/Kids");
const kidsStart = pagesDictionary.indexOf("[", kidsPosition) + 1;
const kidsEnd = pagesDictionary.indexOf("]", kidsPosition);
const pages = pagesDictionary.slice(kidsStart, kidsEnd).toString();
const split = pages.trim().split(' ', 3);
const split = pages.trim().split(" ", 3);
return `${split[0]} ${split[1]} ${split[2]}`;
}
}

View File

@@ -1,13 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = getPagesDictionaryRef;
var _SignPdfError = _interopRequireDefault(require("../../SignPdfError"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
/**
* @param {Object} info As extracted from readRef()
@@ -17,8 +19,11 @@ function getPagesDictionaryRef(info) {
const match = pagesRefRegex.exec(info.root);
if (match === null) {
throw new _SignPdfError.default('Failed to find the pages descriptor. This is probably a problem in node-signpdf.', _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
"Failed to find the pages descriptor. This is probably a problem in node-signpdf.",
_SignPdfError.default.TYPE_PARSE
);
}
return match[1];
}
}

View File

@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
@@ -21,18 +21,24 @@ var _readPdf = _interopRequireDefault(require("./readPdf"));
var _getPageRef = _interopRequireDefault(require("./getPageRef"));
var _createBufferRootWithAcroform = _interopRequireDefault(require("./createBufferRootWithAcroform"));
var _createBufferRootWithAcroform = _interopRequireDefault(
require("./createBufferRootWithAcroform")
);
var _createBufferPageWithAnnotation = _interopRequireDefault(require("./createBufferPageWithAnnotation"));
var _createBufferPageWithAnnotation = _interopRequireDefault(
require("./createBufferPageWithAnnotation")
);
var _createBufferTrailer = _interopRequireDefault(require("./createBufferTrailer"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const isContainBufferRootWithAcroform = pdf => {
const isContainBufferRootWithAcroform = (pdf) => {
const bufferRootWithAcroformRefRegex = /\/AcroForm\s+(\d+\s\d+\sR)/g;
const match = bufferRootWithAcroformRefRegex.exec(pdf.toString());
return match != null && match[1] != null && match[1] !== '';
return match != null && match[1] != null && match[1] !== "";
};
/**
* Adds a signature placeholder to a PDF Buffer.
@@ -44,15 +50,14 @@ const isContainBufferRootWithAcroform = pdf => {
* not only on a freshly created through PDFKit one.
*/
const plainAddPlaceholder = ({
pdfBuffer,
reason,
contactInfo = 'emailfromp1289@gmail.com',
name = 'Name from p12',
location = 'Location from p12',
contactInfo = "emailfromp1289@gmail.com",
name = "Name from p12",
location = "Location from p12",
signatureLength = _const.DEFAULT_SIGNATURE_LENGTH,
subFilter = _const.SUBFILTER_ADOBE_PKCS7_DETACHED
subFilter = _const.SUBFILTER_ADOBE_PKCS7_DETACHED,
}) => {
let pdf = (0, _removeTrailingNewLine.default)(pdfBuffer);
const info = (0, _readPdf.default)(pdf);
@@ -65,24 +70,27 @@ const plainAddPlaceholder = ({
const index = additionalIndex != null ? additionalIndex : info.xref.maxIndex;
addedReferences.set(index, pdf.length + 1); // + 1 new line
pdf = Buffer.concat([pdf, Buffer.from('\n'), Buffer.from(`${index} 0 obj\n`), Buffer.from(_pdfobject.default.convert(input)), Buffer.from('\nendobj\n')]);
pdf = Buffer.concat([
pdf,
Buffer.from("\n"),
Buffer.from(`${index} 0 obj\n`),
Buffer.from(_pdfobject.default.convert(input)),
Buffer.from("\nendobj\n"),
]);
return new _pdfkitReferenceMock.default(info.xref.maxIndex);
},
page: {
dictionary: new _pdfkitReferenceMock.default(pageIndex, {
data: {
Annots: []
}
})
Annots: [],
},
}),
},
_root: {
data: {}
}
data: {},
},
};
const {
form,
widget
} = (0, _pdfkitAddPlaceholder.default)({
const { form, widget } = (0, _pdfkitAddPlaceholder.default)({
pdf: pdfKitMock,
pdfBuffer,
reason,
@@ -90,20 +98,32 @@ const plainAddPlaceholder = ({
name,
location,
signatureLength,
subFilter
subFilter,
});
if (!isContainBufferRootWithAcroform(pdf)) {
const rootIndex = (0, _getIndexFromRef.default)(info.xref, info.rootRef);
addedReferences.set(rootIndex, pdf.length + 1);
pdf = Buffer.concat([pdf, Buffer.from('\n'), (0, _createBufferRootWithAcroform.default)(pdf, info, form)]);
pdf = Buffer.concat([
pdf,
Buffer.from("\n"),
(0, _createBufferRootWithAcroform.default)(pdf, info, form),
]);
}
addedReferences.set(pageIndex, pdf.length + 1);
pdf = Buffer.concat([pdf, Buffer.from('\n'), (0, _createBufferPageWithAnnotation.default)(pdf, info, pageRef, widget)]);
pdf = Buffer.concat([pdf, Buffer.from('\n'), (0, _createBufferTrailer.default)(pdf, info, addedReferences)]);
pdf = Buffer.concat([
pdf,
Buffer.from("\n"),
(0, _createBufferPageWithAnnotation.default)(pdf, info, pageRef, widget),
]);
pdf = Buffer.concat([
pdf,
Buffer.from("\n"),
(0, _createBufferTrailer.default)(pdf, info, addedReferences),
]);
return pdf;
};
var _default = plainAddPlaceholder;
exports.default = _default;
exports.default = _default;

View File

@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
@@ -9,7 +9,9 @@ var _readRefTable = _interopRequireDefault(require("./readRefTable"));
var _findObject = _interopRequireDefault(require("./findObject"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const getValue = (trailer, key) => {
let index = trailer.indexOf(key);
@@ -19,13 +21,16 @@ const getValue = (trailer, key) => {
}
const slice = trailer.slice(index);
index = slice.indexOf('/', 1);
index = slice.indexOf("/", 1);
if (index === -1) {
index = slice.indexOf('>', 1);
index = slice.indexOf(">", 1);
}
return slice.slice(key.length + 1, index).toString().trim(); // key + at least one space
return slice
.slice(key.length + 1, index)
.toString()
.trim(); // key + at least one space
};
/**
* Simplified parsing of a PDF Buffer.
@@ -36,18 +41,17 @@ const getValue = (trailer, key) => {
* @param {Buffer} pdfBuffer
*/
const readPdf = pdfBuffer => {
const readPdf = (pdfBuffer) => {
// Extract the trailer dictionary.
const trailerStart = pdfBuffer.lastIndexOf('trailer'); // The trailer is followed by xref. Then an EOF. EOF's length is 6 characters.
const trailerStart = pdfBuffer.lastIndexOf("trailer"); // The trailer is followed by xref. Then an EOF. EOF's length is 6 characters.
const trailer = pdfBuffer.slice(trailerStart, pdfBuffer.length - 6);
let xRefPosition = trailer.slice(trailer.lastIndexOf('startxref') + 10).toString();
let xRefPosition = trailer.slice(trailer.lastIndexOf("startxref") + 10).toString();
xRefPosition = parseInt(xRefPosition);
const refTable = (0, _readRefTable.default)(pdfBuffer);
const rootRef = getValue(trailer, '/Root');
const rootRef = getValue(trailer, "/Root");
const root = (0, _findObject.default)(pdfBuffer, refTable, rootRef).toString();
const infoRef = getValue(trailer, '/Info');
const infoRef = getValue(trailer, "/Info");
return {
xref: refTable,
rootRef,
@@ -55,9 +59,9 @@ const readPdf = pdfBuffer => {
infoRef,
trailerStart,
previousXrefs: [],
xRefPosition
xRefPosition,
};
};
var _default = readPdf;
exports.default = _default;
exports.default = _default;

View File

@@ -1,20 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.getXref = exports.getLastTrailerPosition = exports.getFullXrefTable = exports.default = void 0;
exports.getXref =
exports.getLastTrailerPosition =
exports.getFullXrefTable =
exports.default =
void 0;
var _SignPdfError = _interopRequireDefault(require("../../SignPdfError"));
var _xrefToRefMap = _interopRequireDefault(require("./xrefToRefMap"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const getLastTrailerPosition = pdf => {
const trailerStart = pdf.lastIndexOf(Buffer.from('trailer', 'utf8'));
const getLastTrailerPosition = (pdf) => {
const trailerStart = pdf.lastIndexOf(Buffer.from("trailer", "utf8"));
const trailer = pdf.slice(trailerStart, pdf.length - 6);
const xRefPosition = trailer.slice(trailer.lastIndexOf(Buffer.from('startxref', 'utf8')) + 10).toString();
const xRefPosition = trailer
.slice(trailer.lastIndexOf(Buffer.from("startxref", "utf8")) + 10)
.toString();
return parseInt(xRefPosition);
};
@@ -23,47 +31,62 @@ exports.getLastTrailerPosition = getLastTrailerPosition;
const getXref = (pdf, position) => {
let refTable = pdf.slice(position); // slice starting from where xref starts
const realPosition = refTable.indexOf(Buffer.from('xref', 'utf8'));
const realPosition = refTable.indexOf(Buffer.from("xref", "utf8"));
if (realPosition === -1) {
throw new _SignPdfError.default(`Could not find xref anywhere at or after ${position}.`, _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
`Could not find xref anywhere at or after ${position}.`,
_SignPdfError.default.TYPE_PARSE
);
}
if (realPosition > 0) {
const prefix = refTable.slice(0, realPosition);
if (prefix.toString().replace(/\s*/g, '') !== '') {
throw new _SignPdfError.default(`Expected xref at ${position} but found other content.`, _SignPdfError.default.TYPE_PARSE);
if (prefix.toString().replace(/\s*/g, "") !== "") {
throw new _SignPdfError.default(
`Expected xref at ${position} but found other content.`,
_SignPdfError.default.TYPE_PARSE
);
}
}
const nextEofPosition = refTable.indexOf(Buffer.from('%%EOF', 'utf8'));
const nextEofPosition = refTable.indexOf(Buffer.from("%%EOF", "utf8"));
if (nextEofPosition === -1) {
throw new _SignPdfError.default('Expected EOF after xref and trailer but could not find one.', _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
"Expected EOF after xref and trailer but could not find one.",
_SignPdfError.default.TYPE_PARSE
);
}
refTable = refTable.slice(0, nextEofPosition);
refTable = refTable.slice(realPosition + 4); // move ahead with the "xref"
refTable = refTable.slice(refTable.indexOf('\n') + 1); // move after the next new line
refTable = refTable.slice(refTable.indexOf("\n") + 1); // move after the next new line
// extract the size
let size = refTable.toString().split('/Size')[1];
let size = refTable.toString().split("/Size")[1];
if (!size) {
throw new _SignPdfError.default('Size not found in xref table.', _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
"Size not found in xref table.",
_SignPdfError.default.TYPE_PARSE
);
}
size = /^\s*(\d+)/.exec(size);
if (size === null) {
throw new _SignPdfError.default('Failed to parse size of xref table.', _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
"Failed to parse size of xref table.",
_SignPdfError.default.TYPE_PARSE
);
}
size = parseInt(size[1]);
const [objects, infos] = refTable.toString().split('trailer');
const isContainingPrev = infos.split('/Prev')[1] != null;
const [objects, infos] = refTable.toString().split("trailer");
const isContainingPrev = infos.split("/Prev")[1] != null;
let prev;
if (isContainingPrev) {
@@ -77,13 +100,13 @@ const getXref = (pdf, position) => {
return {
size,
prev,
xRefContent
xRefContent,
};
};
exports.getXref = getXref;
const getFullXrefTable = pdf => {
const getFullXrefTable = (pdf) => {
const lastTrailerPosition = getLastTrailerPosition(pdf);
const lastXrefTable = getXref(pdf, lastTrailerPosition);
@@ -101,19 +124,18 @@ const getFullXrefTable = pdf => {
* @returns {object}
*/
exports.getFullXrefTable = getFullXrefTable;
const readRefTable = pdf => {
const readRefTable = (pdf) => {
const fullXrefTable = getFullXrefTable(pdf);
const startingIndex = 0;
const maxIndex = Math.max(...fullXrefTable.keys());
return {
startingIndex,
maxIndex,
offsets: fullXrefTable
offsets: fullXrefTable,
};
};
var _default = readRefTable;
exports.default = _default;
exports.default = _default;

Some files were not shown because too many files have changed in this diff Show More