mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 12:32:34 +10:00
Compare commits
112 Commits
0.9-develo
...
test-pr
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fcc435b98 | |||
| 421327432a | |||
| 134e366c27 | |||
| c79592cd0a | |||
| f7cc44f138 | |||
| 60ff4fc992 | |||
| e4e44b7f22 | |||
| 6034e7a21e | |||
| 2a34cc26c6 | |||
| 0ce66a7957 | |||
| 065efabb39 | |||
| e86d4cc719 | |||
| 30c1c76dd7 | |||
| 22e191e98c | |||
| 5db54d3b8c | |||
| 593c317bf1 | |||
| ee4ca018d8 | |||
| e3db462587 | |||
| 739d29d753 | |||
| 964e749039 | |||
| 84b57d715c | |||
| 85f2b5e84a | |||
| 51c63715d6 | |||
| a9ad586035 | |||
| f3b68772a6 | |||
| 2d0fb2879d | |||
| 2291744580 | |||
| 0c3305b11d | |||
| 4031faec46 | |||
| a9cd5e0d94 | |||
| 24e044097c | |||
| 2bdfb884ec | |||
| ff85c294b2 | |||
| 67328b4504 | |||
| 900ec32697 | |||
| 0344ac324c | |||
| b3e89b16bc | |||
| a9befd342c | |||
| 16f6da01c0 | |||
| f8f941a9cd | |||
| e3059cfb34 | |||
| e79a622ddd | |||
| 9945cfb2c7 | |||
| f32c3d999a | |||
| 4bb5064477 | |||
| c655cb52ad | |||
| 3d0d7d1245 | |||
| f96bf757e2 | |||
| 3f897abffa | |||
| df238e2be3 | |||
| 2b83e28e6d | |||
| 6f31dacd74 | |||
| d4324538cc | |||
| 2f2b708bfe | |||
| 7656d4259e | |||
| d509a6178f | |||
| 2fed1a7034 | |||
| de3c500fea | |||
| dc0c78f270 | |||
| a3e17e9f3e | |||
| 7d79e10587 | |||
| 1a37998f39 | |||
| 2d69783ca1 | |||
| b7cc4aed9b | |||
| 3dfa8fc597 | |||
| d5d3b17623 | |||
| 4710176f78 | |||
| f22d4ebeab | |||
| d32b9871db | |||
| 0e9aa4ab62 | |||
| 9e536e95b6 | |||
| a57e0b2b57 | |||
| a1736afc62 | |||
| 91b206e3d7 | |||
| d37dd000af | |||
| 7d6bd00a22 | |||
| 8505b9cd10 | |||
| dd67e1a6f0 | |||
| 9009506bb6 | |||
| 025e6a4eb1 | |||
| 72914c49c4 | |||
| 7d0c91e565 | |||
| 36526119b2 | |||
| 156b7a69e7 | |||
| 4eb4759381 | |||
| 1bfac711ac | |||
| cb2d77c609 | |||
| 253e5cfcfa | |||
| ff16972646 | |||
| 3ba6afabfc | |||
| 3961402c70 | |||
| 7fc228a562 | |||
| 899dd205f2 | |||
| 4a915134e4 | |||
| 266ecf0f8d | |||
| 071398273a | |||
| 6419d22155 | |||
| 738c798dbd | |||
| cd5f6fde32 | |||
| 93654ccae5 | |||
| 7c830d3607 | |||
| 559432cc15 | |||
| 2400c34c71 | |||
| ff977c6bff | |||
| 526be3b906 | |||
| 9f700ad0b2 | |||
| f1dc5687d7 | |||
| 793902ae54 | |||
| f77f101e67 | |||
| 49f36a103b | |||
| 6c7ee3edf4 | |||
| c819ed3cfb |
24
.env.example
24
.env.example
@ -1,7 +1,7 @@
|
||||
# Database
|
||||
# You 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
|
||||
# It is however recommend, that you set up a local Postgres SQL instance
|
||||
# ⚠ WARNING: The test database can be resetted or taken offline at any point
|
||||
# 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)
|
||||
# ⚠ 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=''
|
||||
|
||||
@ -13,9 +13,21 @@ NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
|
||||
NEXTAUTH_SECRET='lorem ipsum sit dolor random string for encryption this could literally be anything'
|
||||
NEXTAUTH_URL='http://localhost:3000'
|
||||
|
||||
# MAIL
|
||||
# MAIL (NODEMAILER)
|
||||
# SENDGRID
|
||||
# Get a Sendgrid Api key here: https://signup.sendgrid.com
|
||||
# You can also configure you own SMTP server using Nodemailer in sendMailts. (currently not possible via config)
|
||||
SENDGRID_API_KEY=''
|
||||
|
||||
# SMTP
|
||||
# Set SMTP credentials to use SMTP instead of the Sendgrid API.
|
||||
SMTP_MAIL_HOST=''
|
||||
SMTP_MAIL_PORT=''
|
||||
SMTP_MAIL_USER=''
|
||||
SMTP_MAIL_PASSWORD=''
|
||||
|
||||
# Sender for signing requests and completion mails.
|
||||
MAIL_FROM=''
|
||||
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
|
||||
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -1,3 +1,3 @@
|
||||
[submodule "apps/website/documenso/website"]
|
||||
path = apps/website/documenso/website
|
||||
url = http://github.com/eltimuro/website.git
|
||||
url = http://github.com/documenso/website.git
|
||||
@ -1,31 +1,37 @@
|
||||
|
||||
# Contributing to Documenso
|
||||
|
||||
If you plan to contribute to Documenso, please take a moment to feel awesome ✨ People like you are what open source is about ♥. Any contributions, no matter how big or small, are highly appreciated.
|
||||
|
||||
## Before getting started
|
||||
|
||||
- Before jumping into a PR be sure to search [existing PRs](https://github.com/documenso/documenso/pulls) or [issues](https://github.com/documenso/documenso/issues) for an open or closed item that relates to your submission.
|
||||
- Select and issue from [here](https://github.com/documenso/documenso/issues) or create a new one
|
||||
- Consider the results from the discussion in the issue
|
||||
|
||||
## Developing
|
||||
The development branch is <code>development</code>. All pull request should be made against this branch. If you need help getting started, [join us on Slack](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w).
|
||||
|
||||
The development branch is <code>main</code>. All pull request should be made against this branch. If you need help getting started, [join us on Slack](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w).
|
||||
|
||||
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
|
||||
own GitHub account and then
|
||||
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||
2. Create a new branch:
|
||||
|
||||
- Create a new branch (include the issue id and somthing readable):
|
||||
|
||||
```sh
|
||||
git checkout -b doc-999-my-feature-or-fix
|
||||
```
|
||||
```sh
|
||||
git checkout -b doc-999-my-feature-or-fix
|
||||
```
|
||||
|
||||
3. See the [Developer Setup](https://github.com/documenso/documenso/blob/main/README.md#developer-setup) for more setup details.
|
||||
## Building
|
||||
|
||||
## Building
|
||||
|
||||
> **Note**
|
||||
> Please be sure that you can make a full production build before pushing code or creating PRs.
|
||||
|
||||
You can build the project with:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
> **Note**
|
||||
> Please be sure that you can make a full production build before pushing code or creating PRs.
|
||||
35
README.md
35
README.md
@ -25,7 +25,7 @@
|
||||
<a href="https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w"><img src="https://img.shields.io/badge/Slack-documenso.slack.com-%234A154B" alt="Join Documenso on Slack"></a>
|
||||
<a href="https://github.com/documenso/documenso/stargazers"><img src="https://img.shields.io/github/stars/documenso/documenso" alt="Github Stars"></a>
|
||||
<a href="https://github.com/documenso/documenso/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
||||
<a href="https://github.com/documenso/documensom/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
|
||||
<a href="https://github.com/documenso/documenso/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
|
||||
</p>
|
||||
|
||||
# Documenso 0.9 - Developer Preview
|
||||
@ -57,6 +57,7 @@
|
||||
Signing documents digitally is fast, easy and should be best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure.
|
||||
|
||||
## Community and Next Steps 🎯
|
||||
|
||||
The current project goal is to <b>[release a production ready version](https://github.com/documenso/documenso/milestone/1)</b> for self-hosting as soon as possible. If you want to help making that happen you can:
|
||||
|
||||
- Check out the first source code release in this repository and test it
|
||||
@ -67,13 +68,11 @@ The current project goal is to <b>[release a production ready version](https://g
|
||||
- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release
|
||||
|
||||
## Contributing
|
||||
|
||||
- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Tools
|
||||
|
||||
- This repos uses 📝 https://gitmoji.dev/ for more expressive commit messages.
|
||||
- Use 🧹 for quality of code (eg remove comments, debug output, remove unused code)
|
||||
|
||||
# Tech
|
||||
|
||||
Documenso is built using awesome open source tech including:
|
||||
@ -105,32 +104,38 @@ To run Documenso locally you need
|
||||
Follow these steps to setup documenso on you local machnine:
|
||||
|
||||
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||
```sh
|
||||
git clone https://github.com/documenso/documenso
|
||||
```
|
||||
```sh
|
||||
git clone https://github.com/documenso/documenso
|
||||
```
|
||||
- Run <code>npm i</code> in root directory
|
||||
- Rename .env.example to .env
|
||||
- Rename <code>.env.example</code> to <code>.env</code>
|
||||
- Set DATABASE_URL value in .env file
|
||||
- You can use the provided test database url (may be wiped at any point)
|
||||
- Or setup a local postgres sql instance (recommened)
|
||||
- Set SENDGRID_API_KEY value in .env file
|
||||
- You need SendGrid account, which you can create [here](https://signup.sendgrid.com/).
|
||||
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own smtp server
|
||||
- Create the database scheme by running <code>db-migrate:dev</code>
|
||||
- Setup your mail provider
|
||||
- Set <code>SENDGRID_API_KEY</code> value in .env file
|
||||
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/).
|
||||
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the <code>SMTP\_\* varibles</code> in your .env
|
||||
- Run <code>npm run dev</code> root directory to start
|
||||
- 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
|
||||
|
||||
- Optional: Create your own signing certificate
|
||||
- A demo certificate is provided in /app/web/ressources/certificate.p12
|
||||
- To generate you own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signging certificate**.
|
||||
|
||||
## Updating
|
||||
|
||||
- If you pull the newest version from main, using <code>git pull</code>, it may be neccessary to regenerate your database client
|
||||
- You can do this by running the generate command in /packages/prisma:
|
||||
```sh
|
||||
npx prisma generate
|
||||
```
|
||||
```sh
|
||||
npx prisma generate
|
||||
```
|
||||
- This is not neccessary on first clone
|
||||
|
||||
# Creating your own signging certificate
|
||||
@ -144,7 +149,7 @@ For the digital signature of you documents you need a signign certificate in .p1
|
||||
<code>openssl req -new -x509 -key private.key -out certificate.crt -days 365</code> \
|
||||
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid.
|
||||
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this: \
|
||||
<code>openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt</code>
|
||||
<code>openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt</code>
|
||||
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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,11 +25,7 @@ export default function FieldTypeSelector(props: any) {
|
||||
value={selectedFieldType}
|
||||
onChange={(e: any) => {
|
||||
setSelectedFieldType(e);
|
||||
}}
|
||||
onMouseDown={(e: any) => {
|
||||
if (e.button === 0) props.setAdding(true);
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<div className="space-y-4">
|
||||
{fieldTypes.map((fieldType) => (
|
||||
<RadioGroup.Option
|
||||
@ -40,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>
|
||||
|
||||
@ -1,11 +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 dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { createField } from "@documenso/features/editor";
|
||||
import RecipientSelector from "./recipient-selector";
|
||||
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"), {
|
||||
@ -17,8 +20,8 @@ export default function PDFEditor(props: any) {
|
||||
const [fields, setFields] = useState<any[]>(props.document.Field);
|
||||
const [selectedRecipient, setSelectedRecipient]: any = useState();
|
||||
const [selectedFieldType, setSelectedFieldType] = useState();
|
||||
const noRecipients = props?.document.Recipient.length === 0;
|
||||
const [adding, setAdding] = useState(false);
|
||||
const noRecipients =
|
||||
props?.document.Recipient.length === 0 || props?.document.Recipient.every((e: any) => !e.email);
|
||||
|
||||
function onPositionChangedHandler(position: any, id: any) {
|
||||
if (!position) return;
|
||||
@ -47,9 +50,31 @@ export default function PDFEditor(props: any) {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<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" />
|
||||
</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.
|
||||
</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">
|
||||
Add Recipients
|
||||
<span aria-hidden="true"> →</span>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PDFViewer
|
||||
style={{
|
||||
cursor: `url("https://place-hold.it/110x64/37f095/FFFFFF&text=${selectedFieldType}") 55 32, auto`,
|
||||
cursor: !noRecipients
|
||||
? `url("https://place-hold.it/110x64/37f095/FFFFFF&text=${selectedFieldType}") 55 32, auto`
|
||||
: "",
|
||||
}}
|
||||
readonly={false}
|
||||
document={props.document}
|
||||
@ -60,27 +85,19 @@ export default function PDFEditor(props: any) {
|
||||
onMouseUp={(e: any, page: number) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log(adding);
|
||||
if (adding) {
|
||||
addField(e, page);
|
||||
setAdding(false);
|
||||
}
|
||||
}}
|
||||
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}
|
||||
/>
|
||||
<hr className="m-3 border-slate-300"></hr>
|
||||
<FieldTypeSelector
|
||||
setAdding={setAdding}
|
||||
selectedRecipient={selectedRecipient}
|
||||
onChange={setSelectedFieldType}
|
||||
/>
|
||||
@ -92,16 +109,12 @@ export default function PDFEditor(props: any) {
|
||||
function addField(e: any, page: number) {
|
||||
if (!selectedRecipient) return;
|
||||
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(fields.concat(res));
|
||||
setFields((prevState) => [...prevState, res]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,22 +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 toast from "react-hot-toast";
|
||||
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,
|
||||
@ -28,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(() => {
|
||||
@ -70,7 +60,7 @@ export default function PDFSigner(props: any) {
|
||||
);
|
||||
const signedField = { ...dialogField };
|
||||
signedField.signature = signature;
|
||||
setFields(fields.concat(signedField));
|
||||
setFields((prevState) => [...prevState, signedField]);
|
||||
setOpen(false);
|
||||
setDialogField(null);
|
||||
}
|
||||
@ -81,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})`
|
||||
@ -97,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>
|
||||
@ -118,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>
|
||||
@ -145,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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -167,15 +148,10 @@ 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).then((res) => {
|
||||
setFields(fields.concat(res));
|
||||
createOrUpdateField(props.document, freeSignatureField, recipient.token).then((res) => {
|
||||
setFields((prevState) => [...prevState, res]);
|
||||
setDialogField(res);
|
||||
setOpen(true);
|
||||
});
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
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 { FieldType } from "@prisma/client";
|
||||
import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
|
||||
import short from "short-uuid";
|
||||
|
||||
export default function PDFViewer(props) {
|
||||
@ -32,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
|
||||
@ -56,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}`}
|
||||
@ -68,28 +66,29 @@ export default function PDFViewer(props) {
|
||||
if (e.height) setPageHeight(e.height);
|
||||
setLoading(false);
|
||||
}}
|
||||
onRenderError={() => setLoading(false)}
|
||||
></Page>
|
||||
onRenderError={() => setLoading(false)}></Page>
|
||||
{props?.fields
|
||||
.filter((item) => item.page === index)
|
||||
.map((item) =>
|
||||
.filter((field) => field.page === index)
|
||||
.map((field) =>
|
||||
props.readonly ? (
|
||||
<SignableField
|
||||
onClick={props.onClick}
|
||||
key={item.id}
|
||||
field={item}
|
||||
key={field.id}
|
||||
field={field}
|
||||
className="absolute"
|
||||
onDelete={onDeleteHandler}
|
||||
></SignableField>
|
||||
onDelete={onDeleteHandler}></SignableField>
|
||||
) : (
|
||||
<EditableField
|
||||
hidden={item.Signature || item.inserted}
|
||||
key={item.id}
|
||||
field={item}
|
||||
hidden={
|
||||
field.Signature ||
|
||||
field.inserted ||
|
||||
field.type === FieldType.FREE_SIGNATURE
|
||||
}
|
||||
key={field.id}
|
||||
field={field}
|
||||
className="absolute"
|
||||
onPositionChanged={onPositionChangedHandler}
|
||||
onDelete={onDeleteHandler}
|
||||
></EditableField>
|
||||
onDelete={onDeleteHandler}></EditableField>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -1,7 +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 Draggable from "react-draggable";
|
||||
|
||||
const stc = require("string-to-color");
|
||||
|
||||
type FieldPropsType = {
|
||||
@ -34,27 +36,28 @@ export default function SignableField(props: FieldPropsType) {
|
||||
defaultPosition={{ x: 0, y: 0 }}
|
||||
cancel="div"
|
||||
onMouseDown={(e: any) => {
|
||||
e.preventDefault();
|
||||
// e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<div
|
||||
onClick={() => {
|
||||
onClick={(e: any) => {
|
||||
if (!field?.signature) props.onClick(props.field);
|
||||
}}
|
||||
ref={nodeRef}
|
||||
className="cursor-pointer opacity-80 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none hover:brightness-50"
|
||||
className={classNames(
|
||||
"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>
|
||||
) : (
|
||||
@ -62,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} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
@ -17,16 +16,12 @@ interface LoginValues {
|
||||
csrfToken: string;
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
export default function Login(props: any) {
|
||||
const router = useRouter();
|
||||
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)) {
|
||||
@ -80,10 +75,7 @@ export default function Login() {
|
||||
</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>
|
||||
@ -97,7 +89,7 @@ export default function Login() {
|
||||
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>
|
||||
@ -112,29 +104,26 @@ export default function Login() {
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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>
|
||||
@ -143,24 +132,29 @@ export default function Login() {
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
>
|
||||
Create a new Account
|
||||
</Link>
|
||||
</p>
|
||||
{props.allowSignup ? (
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Are you new here?{" "}
|
||||
<Link href="/signup" className="text-neon hover:text-neon font-medium">
|
||||
Create a new Account
|
||||
</Link>
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Like Documenso{" "}
|
||||
<Link
|
||||
href="https://documenso.com"
|
||||
className="text-neon hover:text-neon font-medium">
|
||||
Hosted Documenso will be availible soon™
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 = [
|
||||
{
|
||||
@ -34,13 +33,18 @@ const navigation = [
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
href: "/settings",
|
||||
href: "/settings/profile",
|
||||
current: true,
|
||||
icon: WrenchIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const userNavigation = [
|
||||
{ name: "Your Profile", href: "/settings/profile", icon: UserCircleIcon },
|
||||
{
|
||||
name: "Your Profile",
|
||||
href: "/settings/profile",
|
||||
icon: UserCircleIcon,
|
||||
},
|
||||
{
|
||||
name: "Sign out",
|
||||
href: "",
|
||||
@ -95,13 +99,15 @@ export default function TopNavigation() {
|
||||
}, [session]);
|
||||
|
||||
navigation.forEach((element) => {
|
||||
element.current = router.route.endsWith("/" + element.href.split("/")[1]);
|
||||
element.current =
|
||||
router.route.endsWith("/" + element.href.split("/")[1]) ||
|
||||
router.route.includes(element.href.split("/")[1]);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Disclosure as="nav" className="border-b border-gray-200 bg-white">
|
||||
{({ open }) => (
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 justify-between">
|
||||
@ -118,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>
|
||||
))}
|
||||
@ -135,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>
|
||||
@ -145,16 +148,12 @@ 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}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: avatarFromInitials(
|
||||
user?.name || "" || "",
|
||||
40
|
||||
),
|
||||
__html: avatarFromInitials(user?.name || "" || "", 40),
|
||||
}}
|
||||
/>
|
||||
</Menu.Button>
|
||||
@ -166,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}>
|
||||
@ -178,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>
|
||||
)}
|
||||
@ -215,12 +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>
|
||||
))}
|
||||
@ -236,22 +234,23 @@ export default function TopNavigation() {
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<div className="text-base font-medium text-gray-800">
|
||||
{user?.name || ""}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-500">
|
||||
{user?.email}
|
||||
</div>
|
||||
<div className="text-base font-medium text-gray-800">{user?.name || ""}</div>
|
||||
<div className="text-sm font-medium text-gray-500">{user?.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 space-y-1">
|
||||
{userNavigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
onClick={item.click}
|
||||
onClick={
|
||||
item.href.includes("/settings/profile")
|
||||
? () => {
|
||||
close();
|
||||
}
|
||||
: 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>
|
||||
))}
|
||||
|
||||
@ -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,16 +111,14 @@ export default function Setttings() {
|
||||
</aside>
|
||||
|
||||
<form
|
||||
className="divide-y divide-gray-200 lg:col-span-9"
|
||||
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}>
|
||||
{/* 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>
|
||||
@ -132,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
|
||||
@ -146,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
|
||||
@ -163,13 +151,26 @@ export default function Setttings() {
|
||||
name="first-name"
|
||||
id="first-name"
|
||||
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 disabled:bg-neutral-100 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => updateUser(user)}>Save</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
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>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Forgot your passwort? Email <b>hi@documenso.com</b> to reset it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
"formidable": "^3.2.5",
|
||||
"install": "^0.13.0",
|
||||
"next": "13.0.3",
|
||||
"next-auth": "^4.18.3",
|
||||
"next-auth": ">=4.20.1",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-signpdf": "^1.5.0",
|
||||
|
||||
@ -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 couldn’t find the page you’re 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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -18,10 +14,10 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
}
|
||||
|
||||
let user = null;
|
||||
|
||||
let recipient = null;
|
||||
if (recipientToken) {
|
||||
// Request from signing page without login
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
token: recipientToken?.toString(),
|
||||
},
|
||||
@ -37,33 +33,36 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
if (!user) return res.status(401).end();
|
||||
|
||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||
let document: PrismaDocument | null = null;
|
||||
if (recipientToken) {
|
||||
document = await prisma.document.findFirst({
|
||||
where: { id: recipient?.Document?.id },
|
||||
});
|
||||
} else {
|
||||
document = await getDocument(+documentId, req, res);
|
||||
}
|
||||
|
||||
if (!document)
|
||||
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: {
|
||||
Field: {
|
||||
documentId: document.id,
|
||||
documentId: document?.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let signedDocumentAsBase64 = document.document;
|
||||
let signedDocumentAsBase64 = document?.document || "";
|
||||
|
||||
// No need to add a signature, if no one signed yet.
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
@ -36,8 +32,10 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
}
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
const { id: documentId } = req.query;
|
||||
const { token: recipientToken } = req.query;
|
||||
let user = null;
|
||||
if (!recipientToken) user = await getUserFromToken(req, res);
|
||||
if (!user && !recipientToken) return res.status(401).end();
|
||||
const body: {
|
||||
id: number;
|
||||
type: FieldType;
|
||||
@ -48,18 +46,26 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
customText: string;
|
||||
} = req.body;
|
||||
|
||||
if (!user) return;
|
||||
|
||||
const { id: documentId } = req.query;
|
||||
if (!documentId) {
|
||||
res.status(400).send("Missing parameter documentId.");
|
||||
return;
|
||||
return res.status(400).send("Missing parameter documentId.");
|
||||
}
|
||||
|
||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||
if (recipientToken) {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: { token: recipientToken?.toString() },
|
||||
});
|
||||
|
||||
// todo entity ownerships checks
|
||||
if (document.userId !== user.id) {
|
||||
return res.status(401).send("User does not have access to this document.");
|
||||
if (!recipient || recipient?.documentId !== +documentId)
|
||||
return res.status(401).send("Recipient does not have access to this document.");
|
||||
}
|
||||
|
||||
if (user) {
|
||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||
// todo entity ownerships checks
|
||||
if (document.userId !== user.id) {
|
||||
return res.status(401).send("User does not have access to this document.");
|
||||
}
|
||||
}
|
||||
|
||||
const field = await prisma.field.upsert({
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,18 +1,13 @@
|
||||
import {
|
||||
defaultHandler,
|
||||
defaultResponder,
|
||||
getUserFromToken,
|
||||
} 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 existingUser = await getUserFromToken(req, res);
|
||||
const { token: recipientToken } = req.query;
|
||||
const { signatures: signaturesFromBody }: { signatures: any[] } = req.body;
|
||||
|
||||
@ -29,11 +24,19 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
return res.status(401).send("Recipient not found.");
|
||||
}
|
||||
|
||||
const document: PrismaDocument = await getDocument(
|
||||
recipient.documentId,
|
||||
req,
|
||||
res
|
||||
);
|
||||
const document: PrismaDocument = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: recipient.documentId,
|
||||
},
|
||||
include: {
|
||||
Recipient: {
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
},
|
||||
Field: { include: { Recipient: true, Signature: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) res.status(404).end(`No document found.`);
|
||||
|
||||
@ -70,6 +73,15 @@ 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({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
@ -110,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,
|
||||
},
|
||||
});
|
||||
|
||||
@ -124,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();
|
||||
@ -134,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
|
||||
@ -164,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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
// todo remove before launch
|
||||
|
||||
@ -17,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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,16 +12,14 @@ 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";
|
||||
|
||||
type FormValues = {
|
||||
document: File;
|
||||
@ -58,18 +59,17 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
Dashboard
|
||||
</h1>
|
||||
</header>
|
||||
<dl className="mt-8 grid grid-cols-3 xs:grid-cols-2 gap-5">
|
||||
<dl className="mt-8 grid gap-5 md:grid-cols-3 ">
|
||||
{stats.map((item) => (
|
||||
<Link href={item.link} key={item.name}>
|
||||
<div className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6 ">
|
||||
<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 mr-3 h-6 w-6 inline text-neon"
|
||||
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-3xl font-semibold tracking-tight text-gray-900">
|
||||
<dd className="mt-1 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
|
||||
{getStat(item.name, props)}
|
||||
</dd>
|
||||
</div>
|
||||
@ -80,6 +80,7 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
<input
|
||||
id="fileUploadHelper"
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
onChange={(event: any) => {
|
||||
uploadDocument(event);
|
||||
}}
|
||||
@ -90,26 +91,28 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
onClick={() => {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}}
|
||||
className="cursor-pointer relative block w-full rounded-lg border-2 border-dashed border-gray-300 p-12 text-center 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="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 className="mt-2 block text-sm font-medium text-neon">
|
||||
Upload a new PDF document
|
||||
<span id="add_document" className="text-neon mt-2 block text-sm font-medium">
|
||||
Add a new PDF document.
|
||||
</span>
|
||||
</div>
|
||||
<ReactTooltip
|
||||
anchorId="add_document"
|
||||
place="bottom"
|
||||
content="No preparation needed. Any PDF will do."
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@ -138,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) =>
|
||||
|
||||
@ -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,13 +18,8 @@ import {
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { useRouter } from "next/router";
|
||||
import { uploadDocument } from "@documenso/features";
|
||||
import { DocumentStatus } from "@prisma/client";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
import { Button, IconButton, SelectBox } from "@documenso/ui";
|
||||
import { NextPageContext } from "next";
|
||||
import { deleteDocument, getDocuments } from "@documenso/lib/api";
|
||||
|
||||
const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
const router = useRouter();
|
||||
@ -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"
|
||||
@ -406,20 +353,20 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900">No documents</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Get started by creating a new document.
|
||||
Get started by adding a document. Any PDF will do.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
icon={PlusIcon}
|
||||
onClick={() => {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}}
|
||||
>
|
||||
Upload Document
|
||||
}}>
|
||||
Add Document
|
||||
</Button>
|
||||
<input
|
||||
id="fileUploadHelper"
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
onChange={(event: any) => {
|
||||
uploadDocument(event);
|
||||
}}
|
||||
@ -427,6 +374,11 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ReactTooltip
|
||||
anchorId="empty"
|
||||
place="bottom"
|
||||
content="No preparation needed. Any PDF will do."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { ReactElement, useRef, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { Fragment, ReactElement, useRef, useState } from "react";
|
||||
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,
|
||||
@ -14,30 +18,15 @@ import {
|
||||
UserPlusIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
import { Breadcrumb, Button, IconButton } from "@documenso/ui";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import {
|
||||
createOrUpdateRecipient,
|
||||
deleteRecipient,
|
||||
sendSigningRequests,
|
||||
} from "@documenso/lib/api";
|
||||
import {
|
||||
FormProvider,
|
||||
useFieldArray,
|
||||
useForm,
|
||||
useWatch,
|
||||
} from "react-hook-form";
|
||||
import { DocumentStatus, Document as PrismaDocument } from "@prisma/client";
|
||||
import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
type FormValues = {
|
||||
export type FormValues = {
|
||||
signers: { id: number; email: string; name: string }[];
|
||||
};
|
||||
|
||||
const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
const title: string =
|
||||
`"` + props?.document?.title + `"` + "Recipients | Documenso";
|
||||
const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso";
|
||||
const breadcrumbItems = [
|
||||
{
|
||||
title: "Documents",
|
||||
@ -45,15 +34,14 @@ 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",
|
||||
href:
|
||||
NEXT_PUBLIC_WEBAPP_URL +
|
||||
"/documents/" +
|
||||
props.document.id +
|
||||
"/recipients",
|
||||
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients",
|
||||
},
|
||||
];
|
||||
|
||||
@ -85,7 +73,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<div className="mt-10">
|
||||
<div className="mt-10 px-6 sm:px-0">
|
||||
<div>
|
||||
<Breadcrumb document={props.document} items={breadcrumbItems} />
|
||||
</div>
|
||||
@ -96,342 +84,253 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-shrink-0 md:mt-0 md:ml-4">
|
||||
<Button
|
||||
icon={PencilSquareIcon}
|
||||
color="secondary"
|
||||
className="mr-2"
|
||||
href={breadcrumbItems[1].href}
|
||||
>
|
||||
Edit Document
|
||||
</Button>
|
||||
<Button
|
||||
icon={ArrowDownTrayIcon}
|
||||
color="secondary"
|
||||
className="mr-2"
|
||||
href={"/api/documents/" + props.document.id}
|
||||
>
|
||||
href={"/api/documents/" + props.document.id}>
|
||||
Download
|
||||
</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={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
disabled={
|
||||
(formValues.length || 0) === 0 ||
|
||||
!formValues.some(
|
||||
(r: any) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
|
||||
) ||
|
||||
loading
|
||||
}>
|
||||
Send
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-md bg-white shadow mt-10 p-6">
|
||||
<div className="border-b border-gray-200 pb-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900">
|
||||
Signers
|
||||
</h3>
|
||||
<div className="mt-10 overflow-hidden rounded-md bg-white p-4 shadow sm:p-6">
|
||||
<div className="border-b border-gray-200 pb-3 sm:pb-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900 ">Signers</h3>
|
||||
<p className="mt-2 max-w-4xl text-sm text-gray-500">
|
||||
The people who will sign the document.
|
||||
{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="px-0 py-4 w-full hover:bg-green-50 border-0 group"
|
||||
>
|
||||
<div id="container" className="flex w-full">
|
||||
<div
|
||||
className={classNames(
|
||||
"ml-3 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",
|
||||
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
||||
)}
|
||||
>
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-xs font-medium text-gray-900"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
{...register(`signers.${index}.email`, {
|
||||
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
})}
|
||||
defaultValue={item.email}
|
||||
disabled={item.sendStatus === "SENT" || loading}
|
||||
onBlur={() => {
|
||||
if (!errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event: any) => {
|
||||
if (event.key === "Enter")
|
||||
className="group w-full border-0 px-2 py-3 hover:bg-green-50 sm:py-4">
|
||||
<div id="container" className="block w-full lg:flex lg:justify-between">
|
||||
<div className="block space-y-2 md:flex md:space-x-2 md:space-y-0">
|
||||
<div
|
||||
className={classNames(
|
||||
"focus-within:border-neon focus-within:ring-neon rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:ring-1 md:w-[250px]",
|
||||
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
||||
)}>
|
||||
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
{...register(`signers.${index}.email`, {
|
||||
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
})}
|
||||
defaultValue={item.email}
|
||||
disabled={item.sendStatus === "SENT" || loading}
|
||||
onBlur={() => {
|
||||
if (!errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 sm:text-sm outline-none bg-inherit"
|
||||
placeholder="john.dorian@loremipsum.com"
|
||||
/>
|
||||
{errors?.signers?.[index] ? (
|
||||
<p
|
||||
className="mt-2 text-sm text-red-600"
|
||||
id="email-error"
|
||||
>
|
||||
<XMarkIcon className="inline h-5" /> Invalid Email
|
||||
</p>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"ml-3 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",
|
||||
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
||||
)}
|
||||
>
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-xs font-medium text-gray-900"
|
||||
>
|
||||
Name (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register(`signers.${index}.name`)}
|
||||
defaultValue={item.name}
|
||||
disabled={item.sendStatus === "SENT" || loading}
|
||||
onBlur={() => {
|
||||
if (!errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event: any) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!errors?.signers?.[index]
|
||||
)
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 sm:text-sm outline-none bg-inherit"
|
||||
placeholder="John Dorian"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-auto flex">
|
||||
<div key={item.id}>
|
||||
{item.sendStatus === "NOT_SENT" ? (
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="inline-block mt-3 flex-shrink-0 rounded-full bg-gray-200 px-2 py-0.5 text-xs font-medium text-gray-800"
|
||||
>
|
||||
Not Sent
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{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"></CheckIcon>{" "}
|
||||
Sent
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{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>
|
||||
Seen
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{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>
|
||||
Signed
|
||||
</span>
|
||||
</span>
|
||||
}}
|
||||
onKeyDown={(event: any) => {
|
||||
if (event.key === "Enter")
|
||||
if (!errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
className="block w-full border-0 bg-inherit p-0 text-gray-900 placeholder-gray-500 outline-none disabled:bg-neutral-100 sm:text-sm"
|
||||
placeholder="john.dorian@loremipsum.com"
|
||||
/>
|
||||
{errors?.signers?.[index] ? (
|
||||
<p className="mt-2 text-sm text-red-600" id="email-error">
|
||||
<XMarkIcon className="inline h-5" /> Invalid Email
|
||||
</p>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"focus-within:border-neon focus-within:ring-neon rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:ring-1 md:w-[250px]",
|
||||
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
||||
)}>
|
||||
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
|
||||
Name (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register(`signers.${index}.name`)}
|
||||
defaultValue={item.name}
|
||||
disabled={item.sendStatus === "SENT" || loading}
|
||||
onBlur={() => {
|
||||
if (!errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event: any) => {
|
||||
if (event.key === "Enter" && !errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
className="block w-full border-0 bg-inherit p-0 text-gray-900 placeholder-gray-500 outline-none disabled:bg-neutral-100 sm:text-sm"
|
||||
placeholder="John Dorian"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto flex mr-1">
|
||||
<IconButton
|
||||
icon={PaperAirplaneIcon}
|
||||
disabled={
|
||||
!item.id ||
|
||||
item.sendStatus !== "SENT" ||
|
||||
item.signingStatus === "SIGNED" ||
|
||||
loading
|
||||
}
|
||||
color="secondary"
|
||||
className="mr-4 h-9 my-auto"
|
||||
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 className="flex items-center space-x-2 lg:ml-2">
|
||||
<div className="mb-2 mr-2 flex lg:mr-0">
|
||||
<div key={item.id} className="space-x-2">
|
||||
{item.sendStatus === "NOT_SENT" ? (
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800">
|
||||
Not Sent
|
||||
</span>
|
||||
) : null}
|
||||
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
|
||||
<span id="sent_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800 ">
|
||||
<CheckIcon className="mr-1 inline h-5" /> Sent
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
{item.readStatus === "OPENED" && item.signingStatus === "NOT_SIGNED" ? (
|
||||
<span id="read_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800">
|
||||
<CheckIcon className="-mr-2 inline h-5"></CheckIcon>
|
||||
<CheckIcon className="mr-1 inline h-5"></CheckIcon>
|
||||
Seen
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
{item.signingStatus === "SIGNED" ? (
|
||||
<span id="signed_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="mt-3 inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>
|
||||
Signed
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{props.document.status !== DocumentStatus.COMPLETED && (
|
||||
<div className="mr-1 flex">
|
||||
<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);
|
||||
});
|
||||
}
|
||||
}}>
|
||||
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>
|
||||
</div>
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={setOpen}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
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="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-lg sm:p-6">
|
||||
<div>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
||||
<EnvelopeIcon
|
||||
className="h-6 w-6 text-green-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Ready to send
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{`"${props.document.title}" will be sent to ${
|
||||
formValues.filter(
|
||||
(s: any) => s.email && s.sendStatus != "SENT"
|
||||
).length
|
||||
} recipients.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
|
||||
<Button color="secondary" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
setLoading(true);
|
||||
sendSigningRequests(props.document).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<Dialog
|
||||
title="Ready to send"
|
||||
document={props.document}
|
||||
formValues={formValues}
|
||||
open={open}
|
||||
setLoading={setLoading}
|
||||
setOpen={setOpen}
|
||||
icon={<EnvelopeIcon className="h-6 w-6 text-green-600" aria-hidden="true" />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -451,11 +350,7 @@ export async function getServerSideProps(context: any) {
|
||||
};
|
||||
|
||||
const { id: documentId } = context.query;
|
||||
const document: PrismaDocument = await getDocument(
|
||||
+documentId,
|
||||
context.req,
|
||||
context.res
|
||||
);
|
||||
const document: PrismaDocument = await getDocument(+documentId, context.req, context.res);
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
@ -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>
|
||||
@ -107,7 +90,6 @@ export async function getServerSideProps(context: any) {
|
||||
where: {
|
||||
documentId: recipient.Document.id,
|
||||
recipientId: recipient.id,
|
||||
type: { in: [FieldType.SIGNATURE] },
|
||||
Signature: { is: null },
|
||||
},
|
||||
include: {
|
||||
@ -119,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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
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 SignPage: NextPageWithLayout = (props: any) => {
|
||||
const Signed: NextPageWithLayout = (props: any) => {
|
||||
const router = useRouter();
|
||||
const allRecipientsSigned = props.document.Recipient?.every(
|
||||
(r: any) => r.signingStatus === "SIGNED"
|
||||
@ -18,48 +18,35 @@ const SignPage: 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"
|
||||
onClick={(event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
router.push("/api/documents/" + props.document.id);
|
||||
}}
|
||||
>
|
||||
router.push(
|
||||
"/api/documents/" + props.document.id + "?token=" + props.recipient.token
|
||||
);
|
||||
}}>
|
||||
Download "{props.document.title}"
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="relative mx-96">
|
||||
<div
|
||||
className="absolute inset-0 flex items-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="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>
|
||||
@ -67,10 +54,7 @@ const SignPage: 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>
|
||||
@ -103,8 +87,9 @@ export async function getServerSideProps(context: any) {
|
||||
props: {
|
||||
document: JSON.parse(JSON.stringify(recipient.Document)),
|
||||
fields: JSON.parse(JSON.stringify(fields)),
|
||||
recipient: JSON.parse(JSON.stringify(recipient)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default SignPage;
|
||||
export default Signed;
|
||||
|
||||
@ -1,13 +1,23 @@
|
||||
import Head from "next/head";
|
||||
import Login from "../components/login";
|
||||
|
||||
export default function LoginPage() {
|
||||
export default function LoginPage(props: any) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Login | Documenso</title>
|
||||
</Head>
|
||||
<Login></Login>
|
||||
<Login allowSignup={props.ALLOW_SIGNUP}></Login>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const ALLOW_SIGNUP = process.env.ALLOW_SIGNUP === "true";
|
||||
|
||||
return {
|
||||
props: {
|
||||
ALLOW_SIGNUP: ALLOW_SIGNUP,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
import SettingsPage from ".";
|
||||
|
||||
export default SettingsPage;
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
import SettingsPage from ".";
|
||||
|
||||
export default SettingsPage;
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
import SettingsPage from ".";
|
||||
|
||||
export default SettingsPage;
|
||||
|
||||
@ -14,6 +14,14 @@ export default function SignupPage(props: { source: string }) {
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
if (process.env.ALLOW_SIGNUP !== "true")
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const signupSource: string = context.query["source"];
|
||||
return {
|
||||
props: {
|
||||
|
||||
@ -3,4 +3,4 @@ module.exports = {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
BIN
apps/web/ressources/example.pdf
Normal file
BIN
apps/web/ressources/example.pdf
Normal file
Binary file not shown.
@ -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;
|
||||
}
|
||||
|
||||
2650
package-lock.json
generated
2650
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@
|
||||
"dev": "cd apps && cd web && next dev",
|
||||
"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"
|
||||
},
|
||||
@ -24,6 +25,7 @@
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/node": "18.11.9",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@types/react-signature-canvas": "^1.0.2",
|
||||
"avatar-from-initials": "^1.0.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dotenv": "^16.0.3",
|
||||
@ -31,7 +33,7 @@
|
||||
"eslint-config-next": "13.0.3",
|
||||
"install": "^0.13.0",
|
||||
"next": "13.0.3",
|
||||
"next-auth": "^4.18.3",
|
||||
"next-auth": ">=4.20.1",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"npm": "^9.1.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
@ -43,7 +45,8 @@
|
||||
"typescript": "4.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-signature-canvas": "^1.0.2",
|
||||
"file-loader": "^6.2.0"
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-tailwindcss": "^0.2.5"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
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]) {
|
||||
const body = new FormData();
|
||||
const document = event.target.files[0];
|
||||
const fileName = event.target.files[0].name;
|
||||
const fileName: string = event.target.files[0].name;
|
||||
|
||||
if (!fileName.endsWith(".pdf")) {
|
||||
toast.error("Non-PDF documents are not supported yet.");
|
||||
return;
|
||||
}
|
||||
body.append("document", document || "");
|
||||
const response: any = await toast
|
||||
.promise(
|
||||
|
||||
@ -2,11 +2,12 @@ import toast from "react-hot-toast";
|
||||
|
||||
export const createOrUpdateField = async (
|
||||
document: any,
|
||||
field: any
|
||||
field: any,
|
||||
recipientToken: string = ""
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const created = await toast.promise(
|
||||
fetch("/api/documents/" + document.id + "/fields", {
|
||||
fetch("/api/documents/" + document.id + "/fields?token=" + recipientToken, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -11,14 +11,29 @@ export const sendMail = async (
|
||||
content: string | Buffer;
|
||||
}[] = []
|
||||
) => {
|
||||
if (!process.env.SENDGRID_API_KEY)
|
||||
throw new Error("Sendgrid API Key not set.");
|
||||
let transport;
|
||||
if (process.env.SENDGRID_API_KEY)
|
||||
transport = nodemailer.createTransport(
|
||||
nodemailerSendgrid({
|
||||
apiKey: process.env.SENDGRID_API_KEY || "",
|
||||
})
|
||||
);
|
||||
|
||||
if (process.env.SMTP_MAIL_HOST)
|
||||
transport = nodemailer.createTransport({
|
||||
host: process.env.SMTP_MAIL_HOST || "",
|
||||
port: Number(process.env.SMTP_MAIL_PORT) || 587,
|
||||
auth: {
|
||||
user: process.env.SMTP_MAIL_USER || "",
|
||||
pass: process.env.SMTP_MAIL_PASSWORD || "",
|
||||
},
|
||||
});
|
||||
|
||||
if (!transport)
|
||||
throw new Error(
|
||||
"No valid transport for NodeMailer found. Probably Sendgrid API Key nor SMTP Mail host was set."
|
||||
);
|
||||
|
||||
const transport = await nodemailer.createTransport(
|
||||
nodemailerSendgrid({
|
||||
apiKey: process.env.SENDGRID_API_KEY || "",
|
||||
})
|
||||
);
|
||||
await transport
|
||||
.sendMail({
|
||||
from: process.env.MAIL_FROM,
|
||||
|
||||
@ -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"),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
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
|
||||
? `${user.name} (${user.email}) has sent you a document to sign. `
|
||||
: `${user.email} has sent you a document to sign. `;
|
||||
|
||||
export const sendSigningRequest = async (
|
||||
recipient: any,
|
||||
document: any,
|
||||
user: any
|
||||
) => {
|
||||
await sendMail(
|
||||
recipient.email,
|
||||
`Please sign ${document.title}`,
|
||||
signingRequestTemplate(
|
||||
`${user.name} (${user.email}) has sent you a document to sign. `,
|
||||
signingRequestMessage,
|
||||
document,
|
||||
recipient,
|
||||
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${document.id}/sign?token=${recipient.token}`,
|
||||
|
||||
@ -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 = `
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.");
|
||||
|
||||
|
||||
@ -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" });
|
||||
}
|
||||
};
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -12,7 +12,7 @@ export async function getUserFromToken(
|
||||
const tokenEmail = token?.email?.toString();
|
||||
|
||||
if (!token) {
|
||||
res.status(401).send("No session token found for request.");
|
||||
if (res.status) res.status(401).send("No session token found for request.");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IdentityProvider" AS ENUM ('DOCUMENSO', 'GOOGLE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DocumentStatus" AS ENUM ('DRAFT', 'PENDING', 'COMPLETED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ReadStatus" AS ENUM ('NOT_OPENED', 'OPENED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SendStatus" AS ENUM ('NOT_SENT', 'SENT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SigningStatus" AS ENUM ('NOT_SIGNED', 'SIGNED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FieldType" AS ENUM ('SIGNATURE', 'FREE_SIGNATURE', 'DATE', 'TEXT');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT,
|
||||
"email" TEXT NOT NULL,
|
||||
"emailVerified" TIMESTAMP(3),
|
||||
"password" TEXT,
|
||||
"source" TEXT,
|
||||
"identityProvider" "IdentityProvider" NOT NULL DEFAULT 'DOCUMENSO',
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Account" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"providerAccountId" TEXT NOT NULL,
|
||||
"refresh_token" TEXT,
|
||||
"access_token" TEXT,
|
||||
"expires_at" INTEGER,
|
||||
"token_type" TEXT,
|
||||
"scope" TEXT,
|
||||
"id_token" TEXT,
|
||||
"session_state" TEXT,
|
||||
|
||||
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sessionToken" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"expires" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Document" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"status" "DocumentStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"document" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Document_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Recipient" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"documentId" INTEGER NOT NULL,
|
||||
"email" VARCHAR(255) NOT NULL,
|
||||
"name" VARCHAR(255) NOT NULL DEFAULT '',
|
||||
"token" TEXT NOT NULL,
|
||||
"expired" TIMESTAMP(3),
|
||||
"readStatus" "ReadStatus" NOT NULL DEFAULT 'NOT_OPENED',
|
||||
"signingStatus" "SigningStatus" NOT NULL DEFAULT 'NOT_SIGNED',
|
||||
"sendStatus" "SendStatus" NOT NULL DEFAULT 'NOT_SENT',
|
||||
|
||||
CONSTRAINT "Recipient_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Field" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"documentId" INTEGER NOT NULL,
|
||||
"recipientId" INTEGER,
|
||||
"type" "FieldType" NOT NULL,
|
||||
"page" INTEGER NOT NULL,
|
||||
"positionX" INTEGER NOT NULL DEFAULT 0,
|
||||
"positionY" INTEGER NOT NULL DEFAULT 0,
|
||||
"customText" TEXT NOT NULL,
|
||||
"inserted" BOOLEAN NOT NULL,
|
||||
|
||||
CONSTRAINT "Field_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Signature" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"recipientId" INTEGER NOT NULL,
|
||||
"fieldId" INTEGER NOT NULL,
|
||||
"signatureImageAsBase64" TEXT,
|
||||
"typedSignature" TEXT,
|
||||
|
||||
CONSTRAINT "Signature_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Signature_fieldId_key" ON "Signature"("fieldId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Document" ADD CONSTRAINT "Document_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Recipient" ADD CONSTRAINT "Recipient_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Field" ADD CONSTRAINT "Field_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Field" ADD CONSTRAINT "Field_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Signature" ADD CONSTRAINT "Signature_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Signature" ADD CONSTRAINT "Signature_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "Field"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@ -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;
|
||||
3
packages/prisma/migrations/migration_lock.toml
Normal file
3
packages/prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
@ -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)
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -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");
|
||||
|
||||
@ -1 +1 @@
|
||||
export { signDocument } from "./signDocument";
|
||||
export { signDocument } from "./signDocument";
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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]}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user