Compare commits
90 Commits
0.9-develo
...
before-pre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
# 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
|
# 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
|
||||||
# It is however recommend, that you set up a local Postgres SQL instance
|
# Option 2: Set up a local Postgres SQL instance (RECOMMENDED)
|
||||||
# ⚠ WARNING: The test database can be resetted or taken offline at any point
|
# ⚠ 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.
|
# ⚠ WARNING: Please be aware that nothing written to the test databae is private.
|
||||||
DATABASE_URL=''
|
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_SECRET='lorem ipsum sit dolor random string for encryption this could literally be anything'
|
||||||
NEXTAUTH_URL='http://localhost:3000'
|
NEXTAUTH_URL='http://localhost:3000'
|
||||||
|
|
||||||
# MAIL
|
# MAIL (NODEMAILER)
|
||||||
|
# SENDGRID
|
||||||
# Get a Sendgrid Api key here: https://signup.sendgrid.com
|
# 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=''
|
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.
|
# 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"]
|
[submodule "apps/website/documenso/website"]
|
||||||
path = 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
|
# 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.
|
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 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.
|
- 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
|
- 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
|
- Consider the results from the discussion in the issue
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
The development branch is <code>development</code>. All pull request should be made against this branch. If you need help getting started, [join us on Slack](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w).
|
|
||||||
|
The development branch is <code>main</code>. All pull request should be made against this branch. If you need help getting started, [join us on Slack](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w).
|
||||||
|
|
||||||
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
|
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
|
||||||
own GitHub account and then
|
own GitHub account and then
|
||||||
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||||
2. Create a new branch:
|
2. Create a new branch:
|
||||||
|
|
||||||
- Create a new branch (include the issue id and somthing readable):
|
- Create a new branch (include the issue id and somthing readable):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git checkout -b doc-999-my-feature-or-fix
|
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.
|
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:
|
You can build the project with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
> **Note**
|
|
||||||
> Please be sure that you can make a full production build before pushing code or creating PRs.
|
|
||||||
34
README.md
34
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://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/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/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>
|
</p>
|
||||||
|
|
||||||
# Documenso 0.9 - Developer Preview
|
# 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.
|
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 🎯
|
## 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:
|
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
|
- 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
|
- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
|
- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
|
||||||
|
|
||||||
## Tools
|
## 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
|
# Tech
|
||||||
|
|
||||||
Documenso is built using awesome open source tech including:
|
Documenso is built using awesome open source tech including:
|
||||||
@@ -105,32 +104,37 @@ To run Documenso locally you need
|
|||||||
Follow these steps to setup documenso on you local machnine:
|
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.
|
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/documenso/documenso
|
git clone https://github.com/documenso/documenso
|
||||||
```
|
```
|
||||||
- Run <code>npm i</code> in root directory
|
- 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
|
- Set DATABASE_URL value in .env file
|
||||||
- You can use the provided test database url (may be wiped at any point)
|
- You can use the provided test database url (may be wiped at any point)
|
||||||
- Or setup a local postgres sql instance (recommened)
|
- Or setup a local postgres sql instance (recommened)
|
||||||
- Set SENDGRID_API_KEY value in .env file
|
- Create the database scheme by running <code>db-migrate:dev</code>
|
||||||
- You need SendGrid account, which you can create [here](https://signup.sendgrid.com/).
|
- Setup your mail provider
|
||||||
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own smtp server
|
- 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
|
- Run <code>npm run dev</code> root directory to start
|
||||||
- Register a new user at http://localhost:3000/signup
|
- 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
|
- Optional: Create your own signing certificate
|
||||||
- A demo certificate is provided in /app/web/ressources/certificate.p12
|
- 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**.
|
- To generate you own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signging certificate**.
|
||||||
|
|
||||||
## Updating
|
## Updating
|
||||||
|
|
||||||
- If you pull the newest version from main, using <code>git pull</code>, it may be neccessary to regenerate your database client
|
- 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:
|
- You can do this by running the generate command in /packages/prisma:
|
||||||
```sh
|
```sh
|
||||||
npx prisma generate
|
npx prisma generate
|
||||||
```
|
```
|
||||||
- This is not neccessary on first clone
|
- This is not neccessary on first clone
|
||||||
|
|
||||||
# Creating your own signging certificate
|
# Creating your own signging certificate
|
||||||
@@ -144,7 +148,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> \
|
<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.
|
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: \
|
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**)
|
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>
|
5. Place the certificate <code>/apps/web/ressource/certificate.p12</code>
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ export default function FieldTypeSelector(props: any) {
|
|||||||
onChange={(e: any) => {
|
onChange={(e: any) => {
|
||||||
setSelectedFieldType(e);
|
setSelectedFieldType(e);
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e: any) => {
|
|
||||||
if (e.button === 0) props.setAdding(true);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{fieldTypes.map((fieldType) => (
|
{fieldTypes.map((fieldType) => (
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { createOrUpdateField, deleteField } from "@documenso/lib/api";
|
|||||||
import { createField } from "@documenso/features/editor";
|
import { createField } from "@documenso/features/editor";
|
||||||
import RecipientSelector from "./recipient-selector";
|
import RecipientSelector from "./recipient-selector";
|
||||||
import FieldTypeSelector from "./field-type-selector";
|
import FieldTypeSelector from "./field-type-selector";
|
||||||
|
import { InformationCircleIcon } from "@heroicons/react/24/outline";
|
||||||
|
import Link from "next/link";
|
||||||
const stc = require("string-to-color");
|
const stc = require("string-to-color");
|
||||||
|
|
||||||
const PDFViewer = dynamic(() => import("./pdf-viewer"), {
|
const PDFViewer = dynamic(() => import("./pdf-viewer"), {
|
||||||
@@ -17,8 +19,9 @@ export default function PDFEditor(props: any) {
|
|||||||
const [fields, setFields] = useState<any[]>(props.document.Field);
|
const [fields, setFields] = useState<any[]>(props.document.Field);
|
||||||
const [selectedRecipient, setSelectedRecipient]: any = useState();
|
const [selectedRecipient, setSelectedRecipient]: any = useState();
|
||||||
const [selectedFieldType, setSelectedFieldType] = useState();
|
const [selectedFieldType, setSelectedFieldType] = useState();
|
||||||
const noRecipients = props?.document.Recipient.length === 0;
|
const noRecipients =
|
||||||
const [adding, setAdding] = useState(false);
|
props?.document.Recipient.length === 0 ||
|
||||||
|
props?.document.Recipient.every((e: any) => !e.email);
|
||||||
|
|
||||||
function onPositionChangedHandler(position: any, id: any) {
|
function onPositionChangedHandler(position: any, id: any) {
|
||||||
if (!position) return;
|
if (!position) return;
|
||||||
@@ -47,9 +50,41 @@ export default function PDFEditor(props: any) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<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
|
<PDFViewer
|
||||||
style={{
|
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}
|
readonly={false}
|
||||||
document={props.document}
|
document={props.document}
|
||||||
@@ -60,11 +95,6 @@ export default function PDFEditor(props: any) {
|
|||||||
onMouseUp={(e: any, page: number) => {
|
onMouseUp={(e: any, page: number) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
console.log(adding);
|
|
||||||
if (adding) {
|
|
||||||
addField(e, page);
|
|
||||||
setAdding(false);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e: any, page: number) => {
|
onMouseDown={(e: any, page: number) => {
|
||||||
if (e.button === 0) addField(e, page);
|
if (e.button === 0) addField(e, page);
|
||||||
@@ -80,7 +110,6 @@ export default function PDFEditor(props: any) {
|
|||||||
/>
|
/>
|
||||||
<hr className="m-3 border-slate-300"></hr>
|
<hr className="m-3 border-slate-300"></hr>
|
||||||
<FieldTypeSelector
|
<FieldTypeSelector
|
||||||
setAdding={setAdding}
|
|
||||||
selectedRecipient={selectedRecipient}
|
selectedRecipient={selectedRecipient}
|
||||||
onChange={setSelectedFieldType}
|
onChange={setSelectedFieldType}
|
||||||
/>
|
/>
|
||||||
@@ -92,6 +121,7 @@ export default function PDFEditor(props: any) {
|
|||||||
function addField(e: any, page: number) {
|
function addField(e: any, page: number) {
|
||||||
if (!selectedRecipient) return;
|
if (!selectedRecipient) return;
|
||||||
if (!selectedFieldType) return;
|
if (!selectedFieldType) return;
|
||||||
|
if (noRecipients) return;
|
||||||
|
|
||||||
const signatureField = createField(
|
const signatureField = createField(
|
||||||
e,
|
e,
|
||||||
@@ -101,7 +131,7 @@ export default function PDFEditor(props: any) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
createOrUpdateField(props?.document, signatureField).then((res) => {
|
createOrUpdateField(props?.document, signatureField).then((res) => {
|
||||||
setFields(fields.concat(res));
|
setFields((prevState) => [...prevState, res]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
CheckBadgeIcon,
|
CheckBadgeIcon,
|
||||||
InformationCircleIcon,
|
InformationCircleIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { FieldType } from "@prisma/client";
|
import { FieldType } from "@prisma/client";
|
||||||
import {
|
import {
|
||||||
createOrUpdateField,
|
createOrUpdateField,
|
||||||
@@ -70,7 +69,7 @@ export default function PDFSigner(props: any) {
|
|||||||
);
|
);
|
||||||
const signedField = { ...dialogField };
|
const signedField = { ...dialogField };
|
||||||
signedField.signature = signature;
|
signedField.signature = signature;
|
||||||
setFields(fields.concat(signedField));
|
setFields((prevState) => [...prevState, signedField]);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setDialogField(null);
|
setDialogField(null);
|
||||||
}
|
}
|
||||||
@@ -174,8 +173,12 @@ export default function PDFSigner(props: any) {
|
|||||||
FieldType.FREE_SIGNATURE
|
FieldType.FREE_SIGNATURE
|
||||||
);
|
);
|
||||||
|
|
||||||
createOrUpdateField(props.document, freeSignatureField).then((res) => {
|
createOrUpdateField(
|
||||||
setFields(fields.concat(res));
|
props.document,
|
||||||
|
freeSignatureField,
|
||||||
|
recipient.token
|
||||||
|
).then((res) => {
|
||||||
|
setFields((prevState) => [...prevState, res]);
|
||||||
setDialogField(res);
|
setDialogField(res);
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
|
|||||||
import EditableField from "./editable-field";
|
import EditableField from "./editable-field";
|
||||||
import SignableField from "./signable-field";
|
import SignableField from "./signable-field";
|
||||||
import short from "short-uuid";
|
import short from "short-uuid";
|
||||||
|
import { FieldType } from "@prisma/client";
|
||||||
|
|
||||||
export default function PDFViewer(props) {
|
export default function PDFViewer(props) {
|
||||||
const [numPages, setNumPages] = useState(null);
|
const [numPages, setNumPages] = useState(null);
|
||||||
@@ -71,21 +72,25 @@ export default function PDFViewer(props) {
|
|||||||
onRenderError={() => setLoading(false)}
|
onRenderError={() => setLoading(false)}
|
||||||
></Page>
|
></Page>
|
||||||
{props?.fields
|
{props?.fields
|
||||||
.filter((item) => item.page === index)
|
.filter((field) => field.page === index)
|
||||||
.map((item) =>
|
.map((field) =>
|
||||||
props.readonly ? (
|
props.readonly ? (
|
||||||
<SignableField
|
<SignableField
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
key={item.id}
|
key={field.id}
|
||||||
field={item}
|
field={field}
|
||||||
className="absolute"
|
className="absolute"
|
||||||
onDelete={onDeleteHandler}
|
onDelete={onDeleteHandler}
|
||||||
></SignableField>
|
></SignableField>
|
||||||
) : (
|
) : (
|
||||||
<EditableField
|
<EditableField
|
||||||
hidden={item.Signature || item.inserted}
|
hidden={
|
||||||
key={item.id}
|
field.Signature ||
|
||||||
field={item}
|
field.inserted ||
|
||||||
|
field.type === FieldType.FREE_SIGNATURE
|
||||||
|
}
|
||||||
|
key={field.id}
|
||||||
|
field={field}
|
||||||
className="absolute"
|
className="absolute"
|
||||||
onPositionChanged={onPositionChangedHandler}
|
onPositionChanged={onPositionChangedHandler}
|
||||||
onDelete={onDeleteHandler}
|
onDelete={onDeleteHandler}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||||||
import Draggable from "react-draggable";
|
import Draggable from "react-draggable";
|
||||||
import { IconButton } from "@documenso/ui";
|
import { IconButton } from "@documenso/ui";
|
||||||
import { XCircleIcon } from "@heroicons/react/20/solid";
|
import { XCircleIcon } from "@heroicons/react/20/solid";
|
||||||
|
import { classNames } from "@documenso/lib";
|
||||||
const stc = require("string-to-color");
|
const stc = require("string-to-color");
|
||||||
|
|
||||||
type FieldPropsType = {
|
type FieldPropsType = {
|
||||||
@@ -34,22 +35,28 @@ export default function SignableField(props: FieldPropsType) {
|
|||||||
defaultPosition={{ x: 0, y: 0 }}
|
defaultPosition={{ x: 0, y: 0 }}
|
||||||
cancel="div"
|
cancel="div"
|
||||||
onMouseDown={(e: any) => {
|
onMouseDown={(e: any) => {
|
||||||
e.preventDefault();
|
// e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={(e: any) => {
|
||||||
if (!field?.signature) props.onClick(props.field);
|
if (!field?.signature) props.onClick(props.field);
|
||||||
}}
|
}}
|
||||||
ref={nodeRef}
|
ref={nodeRef}
|
||||||
className="cursor-pointer opacity-80 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none hover:brightness-50"
|
className={classNames(
|
||||||
|
"opacity-80 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none",
|
||||||
|
field.type === "SIGNATURE"
|
||||||
|
? "cursor-pointer hover:brightness-50"
|
||||||
|
: "cursor-not-allowed"
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
background: stc(props.field.Recipient.email),
|
background: stc(props.field.Recipient.email),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div hidden={field?.signature} className="font-medium my-4">
|
<div hidden={field?.signature} className="font-medium my-4">
|
||||||
{field.type === "SIGNATURE" ? "SIGN HERE" : ""}
|
{field.type === "SIGNATURE" ? "SIGN HERE" : ""}
|
||||||
|
{field.type === "DATE" ? <small>Date (filled on sign)</small> : ""}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
hidden={!field?.signature}
|
hidden={!field?.signature}
|
||||||
|
|||||||
@@ -17,12 +17,11 @@ interface LoginValues {
|
|||||||
csrfToken: string;
|
csrfToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login(props: any) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const methods = useForm<LoginValues>();
|
const methods = useForm<LoginValues>();
|
||||||
const { register, formState } = methods;
|
const { register, formState } = methods;
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
let callbackUrl =
|
let callbackUrl =
|
||||||
typeof router.query?.callbackUrl === "string"
|
typeof router.query?.callbackUrl === "string"
|
||||||
? router.query.callbackUrl
|
? router.query.callbackUrl
|
||||||
@@ -117,7 +116,6 @@ export default function Login() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<a href="#" className="font-medium text-neon hover:text-neon">
|
<a href="#" className="font-medium text-neon hover:text-neon">
|
||||||
@@ -125,7 +123,6 @@ export default function Login() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -152,15 +149,27 @@ export default function Login() {
|
|||||||
<div className="relative flex justify-center"></div>
|
<div className="relative flex justify-center"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-center text-sm text-gray-600">
|
{props.allowSignup ? (
|
||||||
Are you new here?{" "}
|
<p className="mt-2 text-center text-sm text-gray-600">
|
||||||
<Link
|
Are you new here?{" "}
|
||||||
href="/signup"
|
<Link
|
||||||
className="font-medium text-neon hover:text-neon"
|
href="/signup"
|
||||||
>
|
className="font-medium text-neon hover:text-neon"
|
||||||
Create a new Account
|
>
|
||||||
</Link>
|
Create a new Account
|
||||||
</p>
|
</Link>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="mt-2 text-center text-sm text-gray-600">
|
||||||
|
Like Documenso{" "}
|
||||||
|
<Link
|
||||||
|
href="https://documenso.com"
|
||||||
|
className="font-medium text-neon hover:text-neon"
|
||||||
|
>
|
||||||
|
Hosted Documenso will be availible soon™
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,13 +34,18 @@ const navigation = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
href: "/settings",
|
href: "/settings/profile",
|
||||||
current: true,
|
current: true,
|
||||||
icon: WrenchIcon,
|
icon: WrenchIcon,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const userNavigation = [
|
const userNavigation = [
|
||||||
{ name: "Your Profile", href: "/settings/profile", icon: UserCircleIcon },
|
{
|
||||||
|
name: "Your Profile",
|
||||||
|
href: "/settings/profile",
|
||||||
|
icon: UserCircleIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Sign out",
|
name: "Sign out",
|
||||||
href: "",
|
href: "",
|
||||||
@@ -95,13 +100,15 @@ export default function TopNavigation() {
|
|||||||
}, [session]);
|
}, [session]);
|
||||||
|
|
||||||
navigation.forEach((element) => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Disclosure as="nav" className="border-b border-gray-200 bg-white">
|
<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="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex h-16 justify-between">
|
<div className="flex h-16 justify-between">
|
||||||
@@ -151,10 +158,7 @@ export default function TopNavigation() {
|
|||||||
<div
|
<div
|
||||||
key={user?.email}
|
key={user?.email}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: avatarFromInitials(
|
__html: avatarFromInitials(user?.name || "" || "", 40),
|
||||||
user?.name || "" || "",
|
|
||||||
40
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
@@ -220,6 +224,9 @@ export default function TopNavigation() {
|
|||||||
"block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
|
"block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
|
||||||
)}
|
)}
|
||||||
aria-current={item.current ? "page" : undefined}
|
aria-current={item.current ? "page" : undefined}
|
||||||
|
onClick={() => {
|
||||||
|
close();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -236,19 +243,21 @@ export default function TopNavigation() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<div className="text-base font-medium text-gray-800">
|
<div className="text-base font-medium text-gray-800">{user?.name || ""}</div>
|
||||||
{user?.name || ""}
|
<div className="text-sm font-medium text-gray-500">{user?.email}</div>
|
||||||
</div>
|
|
||||||
<div className="text-sm font-medium text-gray-500">
|
|
||||||
{user?.email}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 space-y-1">
|
<div className="mt-3 space-y-1">
|
||||||
{userNavigation.map((item) => (
|
{userNavigation.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.name}
|
key={item.name}
|
||||||
onClick={item.click}
|
onClick={
|
||||||
|
item.href.includes("/settings/profile")
|
||||||
|
? () => {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
: item.click
|
||||||
|
}
|
||||||
href={item.href}
|
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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -115,9 +115,13 @@ export default function Setttings() {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
className="divide-y divide-gray-200 lg:col-span-9"
|
className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
|
||||||
action="#"
|
action="#"
|
||||||
method="POST"
|
method="POST"
|
||||||
|
hidden={
|
||||||
|
subNavigation.filter((e) => e.current)[0]?.name !==
|
||||||
|
subNavigation[0].name
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Profile section */}
|
{/* Profile section */}
|
||||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
@@ -163,13 +167,33 @@ export default function Setttings() {
|
|||||||
name="first-name"
|
name="first-name"
|
||||||
id="first-name"
|
id="first-name"
|
||||||
autoComplete="given-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="mt-1 block w-full rounded-md border disabled:bg-neutral-100 border-gray-300 py-2 px-3 shadow-sm focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => updateUser(user)}>Save</Button>
|
<Button onClick={() => updateUser(user)}>Save</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<div
|
||||||
|
hidden={
|
||||||
|
subNavigation.filter((e) => e.current)[0]?.name !==
|
||||||
|
subNavigation[1].name
|
||||||
|
}
|
||||||
|
className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
|
||||||
|
>
|
||||||
|
{/* Passwords section */}
|
||||||
|
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-medium leading-6 text-gray-900">
|
||||||
|
Password
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
Forgot your passwort? Email <b>hi@documenso.com</b> to reset
|
||||||
|
it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"formidable": "^3.2.5",
|
"formidable": "^3.2.5",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"next": "13.0.3",
|
"next": "13.0.3",
|
||||||
"next-auth": "^4.18.3",
|
"next-auth": ">=4.20.1",
|
||||||
"next-transpile-modules": "^10.0.0",
|
"next-transpile-modules": "^10.0.0",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"node-signpdf": "^1.5.0",
|
"node-signpdf": "^1.5.0",
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let user = null;
|
let user = null;
|
||||||
|
let recipient = null;
|
||||||
if (recipientToken) {
|
if (recipientToken) {
|
||||||
// Request from signing page without login
|
// Request from signing page without login
|
||||||
const recipient = await prisma.recipient.findFirst({
|
recipient = await prisma.recipient.findFirst({
|
||||||
where: {
|
where: {
|
||||||
token: recipientToken?.toString(),
|
token: recipientToken?.toString(),
|
||||||
},
|
},
|
||||||
@@ -37,7 +37,14 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
if (!user) return res.status(401).end();
|
if (!user) return res.status(401).end();
|
||||||
|
|
||||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
let document: PrismaDocument | null = null;
|
||||||
|
if (recipientToken) {
|
||||||
|
document = await prisma.document.findFirst({
|
||||||
|
where: { id: recipient?.Document?.id },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document = await getDocument(+documentId, req, res);
|
||||||
|
}
|
||||||
|
|
||||||
if (!document)
|
if (!document)
|
||||||
res.status(404).end(`No document with id ${documentId} found.`);
|
res.status(404).end(`No document with id ${documentId} found.`);
|
||||||
@@ -45,16 +52,18 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const signaturesCount = await prisma.signature.count({
|
const signaturesCount = await prisma.signature.count({
|
||||||
where: {
|
where: {
|
||||||
Field: {
|
Field: {
|
||||||
documentId: document.id,
|
documentId: document?.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let signedDocumentAsBase64 = document.document;
|
let signedDocumentAsBase64 = document?.document || "";
|
||||||
|
|
||||||
// No need to add a signature, if no one signed yet.
|
// No need to add a signature, if no one signed yet.
|
||||||
if (signaturesCount > 0) {
|
if (signaturesCount > 0) {
|
||||||
signedDocumentAsBase64 = await addDigitalSignature(document.document);
|
signedDocumentAsBase64 = await addDigitalSignature(
|
||||||
|
document?.document || ""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64");
|
const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64");
|
||||||
@@ -62,7 +71,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
res.setHeader("Content-Length", buffer.length);
|
res.setHeader("Content-Length", buffer.length);
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
"Content-Disposition",
|
"Content-Disposition",
|
||||||
`attachment; filename=${document.title}`
|
`attachment; filename=${document?.title}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send(buffer);
|
return res.status(200).send(buffer);
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const user = await getUserFromToken(req, res);
|
const { token: recipientToken } = req.query;
|
||||||
const { id: documentId } = req.query;
|
let user = null;
|
||||||
|
if (!recipientToken) user = await getUserFromToken(req, res);
|
||||||
|
if (!user && !recipientToken) return res.status(401).end();
|
||||||
const body: {
|
const body: {
|
||||||
id: number;
|
id: number;
|
||||||
type: FieldType;
|
type: FieldType;
|
||||||
@@ -48,18 +50,30 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
customText: string;
|
customText: string;
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!user) return;
|
const { id: documentId } = req.query;
|
||||||
|
|
||||||
if (!documentId) {
|
if (!documentId) {
|
||||||
res.status(400).send("Missing parameter documentId.");
|
return res.status(400).send("Missing parameter documentId.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
if (recipientToken) {
|
||||||
|
const recipient = await prisma.recipient.findFirst({
|
||||||
|
where: { token: recipientToken?.toString() },
|
||||||
|
});
|
||||||
|
|
||||||
// todo entity ownerships checks
|
if (!recipient || recipient?.documentId !== +documentId)
|
||||||
if (document.userId !== user.id) {
|
return res
|
||||||
return res.status(401).send("User does not have access to this document.");
|
.status(401)
|
||||||
|
.send("Recipient does not have access to this document.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||||
|
// todo entity ownerships checks
|
||||||
|
if (document.userId !== user.id) {
|
||||||
|
return res
|
||||||
|
.status(401)
|
||||||
|
.send("User does not have access to this document.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const field = await prisma.field.upsert({
|
const field = await prisma.field.upsert({
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||||
defaultHandler,
|
|
||||||
defaultResponder,
|
|
||||||
getUserFromToken,
|
|
||||||
} from "@documenso/lib/server";
|
|
||||||
import prisma from "@documenso/prisma";
|
import prisma from "@documenso/prisma";
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { SigningStatus, DocumentStatus } from "@prisma/client";
|
import { SigningStatus, DocumentStatus } from "@prisma/client";
|
||||||
@@ -12,7 +8,6 @@ import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
|
|||||||
import { sendSigningDoneMail } from "@documenso/lib/mail";
|
import { sendSigningDoneMail } from "@documenso/lib/mail";
|
||||||
|
|
||||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const existingUser = await getUserFromToken(req, res);
|
|
||||||
const { token: recipientToken } = req.query;
|
const { token: recipientToken } = req.query;
|
||||||
const { signatures: signaturesFromBody }: { signatures: any[] } = req.body;
|
const { signatures: signaturesFromBody }: { signatures: any[] } = req.body;
|
||||||
|
|
||||||
@@ -29,11 +24,19 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
return res.status(401).send("Recipient not found.");
|
return res.status(401).send("Recipient not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const document: PrismaDocument = await getDocument(
|
const document: PrismaDocument = await prisma.document.findFirstOrThrow({
|
||||||
recipient.documentId,
|
where: {
|
||||||
req,
|
id: recipient.documentId,
|
||||||
res
|
},
|
||||||
);
|
include: {
|
||||||
|
Recipient: {
|
||||||
|
orderBy: {
|
||||||
|
id: "asc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Field: { include: { Recipient: true, Signature: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!document) res.status(404).end(`No document found.`);
|
if (!document) res.status(404).end(`No document found.`);
|
||||||
|
|
||||||
@@ -70,6 +73,8 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Don't check for inserted, because currently no "sign again" scenarios exist and
|
||||||
|
// this is probably the expected behaviour in unclean states.
|
||||||
const nonSignatureFields = await prisma.field.findMany({
|
const nonSignatureFields = await prisma.field.findMany({
|
||||||
where: {
|
where: {
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||||
defaultHandler,
|
|
||||||
defaultResponder,
|
|
||||||
getUserFromToken,
|
|
||||||
} from "@documenso/lib/server";
|
|
||||||
import prisma from "@documenso/prisma";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { Document as PrismaDocument } from "@prisma/client";
|
import { Document as PrismaDocument } from "@prisma/client";
|
||||||
import { getDocument } from "@documenso/lib/query";
|
import { getDocument } from "@documenso/lib/query";
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import { getUserFromToken } from "@documenso/lib/server";
|
import { getUserFromToken } from "@documenso/lib/server";
|
||||||
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
|
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
|
||||||
import { truncate } from "fs";
|
import { truncate } from "fs";
|
||||||
|
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||||
|
|
||||||
type FormValues = {
|
type FormValues = {
|
||||||
document: File;
|
document: File;
|
||||||
@@ -58,18 +59,18 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
|||||||
Dashboard
|
Dashboard
|
||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
<dl className="mt-8 grid grid-cols-3 xs:grid-cols-2 gap-5">
|
<dl className="grid gap-5 mt-8 md:grid-cols-3 ">
|
||||||
{stats.map((item) => (
|
{stats.map((item) => (
|
||||||
<Link href={item.link} key={item.name}>
|
<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="px-4 py-3 overflow-hidden bg-white rounded-lg shadow md:p-6 sm:py-5">
|
||||||
<dt className="truncate text-sm font-medium text-gray-500 ">
|
<dt className="text-sm font-medium text-gray-500 truncate ">
|
||||||
<item.icon
|
<item.icon
|
||||||
className="flex-shrink-0 mr-3 h-6 w-6 inline text-neon"
|
className="flex-shrink-0 inline w-5 h-5 mr-3 text-neon sm:w-6 sm:h-6"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></item.icon>
|
></item.icon>
|
||||||
{item.name}
|
{item.name}
|
||||||
</dt>
|
</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)}
|
{getStat(item.name, props)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,6 +81,7 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
|||||||
<input
|
<input
|
||||||
id="fileUploadHelper"
|
id="fileUploadHelper"
|
||||||
type="file"
|
type="file"
|
||||||
|
accept="application/pdf"
|
||||||
onChange={(event: any) => {
|
onChange={(event: any) => {
|
||||||
uploadDocument(event);
|
uploadDocument(event);
|
||||||
}}
|
}}
|
||||||
@@ -90,10 +92,10 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
document?.getElementById("fileUploadHelper")?.click();
|
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="relative block w-full p-12 text-center border-2 border-gray-300 border-dashed rounded-lg cursor-pointer hover:border-neon focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="mx-auto h-12 w-12 text-gray-400"
|
className="w-12 h-12 mx-auto text-gray-400"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 00 20 25"
|
viewBox="0 00 20 25"
|
||||||
@@ -105,11 +107,18 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
|||||||
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"
|
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>
|
</svg>
|
||||||
|
<span
|
||||||
<span className="mt-2 block text-sm font-medium text-neon">
|
id="add_document"
|
||||||
Upload a new PDF document
|
className="mt-2 block text-sm font-medium text-neon"
|
||||||
|
>
|
||||||
|
Add a new PDF document.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<ReactTooltip
|
||||||
|
anchorId="add_document"
|
||||||
|
place="bottom"
|
||||||
|
content="No preparation needed. Any PDF will do."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import {
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { uploadDocument } from "@documenso/features";
|
import { uploadDocument } from "@documenso/features";
|
||||||
import { DocumentStatus } from "@prisma/client";
|
import { DocumentStatus } from "@prisma/client";
|
||||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
|
||||||
import { Button, IconButton, SelectBox } from "@documenso/ui";
|
import { Button, IconButton, SelectBox } from "@documenso/ui";
|
||||||
import { NextPageContext } from "next";
|
import { NextPageContext } from "next";
|
||||||
import { deleteDocument, getDocuments } from "@documenso/lib/api";
|
import { deleteDocument, getDocuments } from "@documenso/lib/api";
|
||||||
|
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||||
|
|
||||||
const DocumentsPage: NextPageWithLayout = (props: any) => {
|
const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -406,7 +406,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
|
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900">No documents</h3>
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No documents</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<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>
|
</p>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<Button
|
<Button
|
||||||
@@ -415,11 +415,12 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
document?.getElementById("fileUploadHelper")?.click();
|
document?.getElementById("fileUploadHelper")?.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Upload Document
|
Add Document
|
||||||
</Button>
|
</Button>
|
||||||
<input
|
<input
|
||||||
id="fileUploadHelper"
|
id="fileUploadHelper"
|
||||||
type="file"
|
type="file"
|
||||||
|
accept="application/pdf"
|
||||||
onChange={(event: any) => {
|
onChange={(event: any) => {
|
||||||
uploadDocument(event);
|
uploadDocument(event);
|
||||||
}}
|
}}
|
||||||
@@ -427,6 +428,11 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ReactTooltip
|
||||||
|
anchorId="empty"
|
||||||
|
place="bottom"
|
||||||
|
content="No preparation needed. Any PDF will do."
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { Fragment, ReactElement, useRef, useState } from "react";
|
import { ReactElement, useRef, useState } from "react";
|
||||||
import Layout from "../../../components/layout";
|
import Layout from "../../../components/layout";
|
||||||
import { NextPageWithLayout } from "../../_app";
|
import { NextPageWithLayout } from "../../_app";
|
||||||
import { classNames, NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
|
import { classNames, NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
|
||||||
@@ -7,37 +7,27 @@ import {
|
|||||||
ArrowDownTrayIcon,
|
ArrowDownTrayIcon,
|
||||||
CheckBadgeIcon,
|
CheckBadgeIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
EnvelopeIcon,
|
|
||||||
PaperAirplaneIcon,
|
PaperAirplaneIcon,
|
||||||
PencilSquareIcon,
|
PencilSquareIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
|
EnvelopeIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { getUserFromToken } from "@documenso/lib/server";
|
import { getUserFromToken } from "@documenso/lib/server";
|
||||||
import { getDocument } from "@documenso/lib/query";
|
import { getDocument } from "@documenso/lib/query";
|
||||||
import { Document as PrismaDocument } from "@prisma/client";
|
import { Document as PrismaDocument, DocumentStatus } from "@prisma/client";
|
||||||
import { Breadcrumb, Button, IconButton } from "@documenso/ui";
|
import { Breadcrumb, Button, Dialog, IconButton } from "@documenso/ui";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { createOrUpdateRecipient, deleteRecipient, sendSigningRequests } from "@documenso/lib/api";
|
||||||
import {
|
|
||||||
createOrUpdateRecipient,
|
|
||||||
deleteRecipient,
|
|
||||||
sendSigningRequests,
|
|
||||||
} from "@documenso/lib/api";
|
|
||||||
import {
|
|
||||||
FormProvider,
|
|
||||||
useFieldArray,
|
|
||||||
useForm,
|
|
||||||
useWatch,
|
|
||||||
} from "react-hook-form";
|
|
||||||
|
|
||||||
type FormValues = {
|
import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
export type FormValues = {
|
||||||
signers: { id: number; email: string; name: string }[];
|
signers: { id: number; email: string; name: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const RecipientsPage: NextPageWithLayout = (props: any) => {
|
const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||||
const title: string =
|
const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso";
|
||||||
`"` + props?.document?.title + `"` + "Recipients | Documenso";
|
|
||||||
const breadcrumbItems = [
|
const breadcrumbItems = [
|
||||||
{
|
{
|
||||||
title: "Documents",
|
title: "Documents",
|
||||||
@@ -49,11 +39,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Recipients",
|
title: "Recipients",
|
||||||
href:
|
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients",
|
||||||
NEXT_PUBLIC_WEBAPP_URL +
|
|
||||||
"/documents/" +
|
|
||||||
props.document.id +
|
|
||||||
"/recipients",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -85,25 +71,17 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
<Head>
|
<Head>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="mt-10">
|
<div className="px-6 mt-10 sm:px-0">
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumb document={props.document} items={breadcrumbItems} />
|
<Breadcrumb document={props.document} items={breadcrumbItems} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 md:flex md:items-center md:justify-between">
|
<div className="mt-2 md:flex md:items-center md:justify-between">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="flex-1 min-w-0">
|
||||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||||
{props.document.title}
|
{props.document.title}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex flex-shrink-0 md:mt-0 md:ml-4">
|
<div className="flex flex-shrink-0 mt-4 md:mt-0 md:ml-4">
|
||||||
<Button
|
|
||||||
icon={PencilSquareIcon}
|
|
||||||
color="secondary"
|
|
||||||
className="mr-2"
|
|
||||||
href={breadcrumbItems[1].href}
|
|
||||||
>
|
|
||||||
Edit Document
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
icon={ArrowDownTrayIcon}
|
icon={ArrowDownTrayIcon}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@@ -112,6 +90,15 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
>
|
>
|
||||||
Download
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={PencilSquareIcon}
|
||||||
|
disabled={props.document.status === DocumentStatus.COMPLETED}
|
||||||
|
color={props.document.status === DocumentStatus.COMPLETED ? "primary" : "secondary"}
|
||||||
|
className="mr-2"
|
||||||
|
href={breadcrumbItems[1].href}
|
||||||
|
>
|
||||||
|
Edit Document
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="min-w-[125px]"
|
className="min-w-[125px]"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -122,8 +109,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
disabled={
|
disabled={
|
||||||
(formValues.length || 0) === 0 ||
|
(formValues.length || 0) === 0 ||
|
||||||
!formValues.some(
|
!formValues.some(
|
||||||
(r: any) =>
|
(r: any) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
|
||||||
r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
|
|
||||||
) ||
|
) ||
|
||||||
loading
|
loading
|
||||||
}
|
}
|
||||||
@@ -132,12 +118,10 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-hidden rounded-md bg-white shadow mt-10 p-6">
|
<div className="p-4 mt-10 overflow-hidden bg-white rounded-md shadow sm:p-6">
|
||||||
<div className="border-b border-gray-200 pb-5">
|
<div className="pb-3 border-b border-gray-200 sm:pb-5">
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900">
|
<h3 className="text-lg font-medium leading-6 text-gray-900 ">Signers</h3>
|
||||||
Signers
|
<p className="max-w-4xl mt-2 text-sm text-gray-500">
|
||||||
</h3>
|
|
||||||
<p className="mt-2 max-w-4xl text-sm text-gray-500">
|
|
||||||
The people who will sign the document.
|
The people who will sign the document.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,189 +135,174 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
{fields.map((item: any, index: number) => (
|
{fields.map((item: any, index: number) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className="px-0 py-4 w-full hover:bg-green-50 border-0 group"
|
className="w-full px-2 py-3 border-0 hover:bg-green-50 group sm:py-4"
|
||||||
>
|
>
|
||||||
<div id="container" className="flex w-full">
|
<div id="container" className="block w-full lg:flex lg:justify-between">
|
||||||
<div
|
<div className="block space-y-2 md:space-x-2 md:space-y-0 md:flex">
|
||||||
className={classNames(
|
<div
|
||||||
"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",
|
className={classNames(
|
||||||
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
"md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
|
||||||
)}
|
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
||||||
>
|
)}
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block text-xs font-medium text-gray-900"
|
|
||||||
>
|
>
|
||||||
Email
|
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
|
||||||
</label>
|
Email
|
||||||
<input
|
</label>
|
||||||
type="email"
|
<input
|
||||||
{...register(`signers.${index}.email`, {
|
type="email"
|
||||||
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
{...register(`signers.${index}.email`, {
|
||||||
})}
|
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||||
defaultValue={item.email}
|
})}
|
||||||
disabled={item.sendStatus === "SENT" || loading}
|
defaultValue={item.email}
|
||||||
onBlur={() => {
|
disabled={item.sendStatus === "SENT" || loading}
|
||||||
if (!errors?.signers?.[index])
|
onBlur={() => {
|
||||||
createOrUpdateRecipient({
|
|
||||||
...formValues[index],
|
|
||||||
documentId: props.document.id,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onKeyDown={(event: any) => {
|
|
||||||
if (event.key === "Enter")
|
|
||||||
if (!errors?.signers?.[index])
|
if (!errors?.signers?.[index])
|
||||||
createOrUpdateRecipient({
|
createOrUpdateRecipient({
|
||||||
...formValues[index],
|
...formValues[index],
|
||||||
documentId: props.document.id,
|
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"
|
onKeyDown={(event: any) => {
|
||||||
placeholder="john.dorian@loremipsum.com"
|
if (event.key === "Enter")
|
||||||
/>
|
if (!errors?.signers?.[index])
|
||||||
{errors?.signers?.[index] ? (
|
createOrUpdateRecipient({
|
||||||
<p
|
...formValues[index],
|
||||||
className="mt-2 text-sm text-red-600"
|
documentId: props.document.id,
|
||||||
id="email-error"
|
});
|
||||||
>
|
}}
|
||||||
<XMarkIcon className="inline h-5" /> Invalid Email
|
className="block w-full p-0 text-gray-900 placeholder-gray-500 disabled:bg-neutral-100 border-0 outline-none sm:text-sm bg-inherit"
|
||||||
</p>
|
placeholder="john.dorian@loremipsum.com"
|
||||||
) : (
|
/>
|
||||||
""
|
{errors?.signers?.[index] ? (
|
||||||
)}
|
<p className="mt-2 text-sm text-red-600" id="email-error">
|
||||||
</div>
|
<XMarkIcon className="inline h-5" /> Invalid Email
|
||||||
<div
|
</p>
|
||||||
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>
|
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div className="ml-auto flex mr-1">
|
className={classNames(
|
||||||
<IconButton
|
"md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
|
||||||
icon={PaperAirplaneIcon}
|
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
||||||
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
|
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
|
||||||
</IconButton>
|
Name (optional)
|
||||||
<IconButton
|
</label>
|
||||||
icon={TrashIcon}
|
<input
|
||||||
disabled={
|
type="text"
|
||||||
!item.id || item.sendStatus === "SENT" || loading
|
{...register(`signers.${index}.name`)}
|
||||||
}
|
defaultValue={item.name}
|
||||||
onClick={() => {
|
disabled={item.sendStatus === "SENT" || loading}
|
||||||
const removedItem = { ...fields }[index];
|
onBlur={() => {
|
||||||
remove(index);
|
if (!errors?.signers?.[index])
|
||||||
deleteRecipient(item)?.catch((err) => {
|
createOrUpdateRecipient({
|
||||||
append(removedItem);
|
...formValues[index],
|
||||||
});
|
documentId: props.document.id,
|
||||||
}}
|
});
|
||||||
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
}}
|
||||||
/>
|
onKeyDown={(event: any) => {
|
||||||
|
if (event.key === "Enter" && !errors?.signers?.[index])
|
||||||
|
createOrUpdateRecipient({
|
||||||
|
...formValues[index],
|
||||||
|
documentId: props.document.id,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="block w-full p-0 text-gray-900 placeholder-gray-500 disabled:bg-neutral-100 border-0 outline-none sm:text-sm bg-inherit"
|
||||||
|
placeholder="John Dorian"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 lg:ml-2">
|
||||||
|
<div className="flex mb-2 mr-2 lg:mr-0">
|
||||||
|
<div key={item.id} className="space-x-2">
|
||||||
|
{item.sendStatus === "NOT_SENT" ? (
|
||||||
|
<span
|
||||||
|
id="sent_icon"
|
||||||
|
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
|
||||||
|
>
|
||||||
|
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" /> 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>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex mr-1">
|
||||||
|
<IconButton
|
||||||
|
icon={PaperAirplaneIcon}
|
||||||
|
disabled={
|
||||||
|
!item.id ||
|
||||||
|
item.sendStatus !== "SENT" ||
|
||||||
|
item.signingStatus === "SIGNED" ||
|
||||||
|
loading
|
||||||
|
}
|
||||||
|
color="secondary"
|
||||||
|
className="my-auto mr-4 h-9"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm("Resend this signing request?")) {
|
||||||
|
setLoading(true);
|
||||||
|
sendSigningRequests(props.document, [item.id]).finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
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>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -359,79 +328,16 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<Dialog
|
||||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
title="Ready to send"
|
||||||
<Transition.Child
|
document={props.document}
|
||||||
as={Fragment}
|
formValues={formValues}
|
||||||
enter="ease-out duration-300"
|
open={open}
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
setLoading={setLoading}
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
setOpen={setOpen}
|
||||||
leave="ease-in duration-200"
|
icon={<EnvelopeIcon className="w-6 h-6 text-green-600" aria-hidden="true" />}
|
||||||
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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -451,11 +357,7 @@ export async function getServerSideProps(context: any) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { id: documentId } = context.query;
|
const { id: documentId } = context.query;
|
||||||
const document: PrismaDocument = await getDocument(
|
const document: PrismaDocument = await getDocument(+documentId, context.req, context.res);
|
||||||
+documentId,
|
|
||||||
context.req,
|
|
||||||
context.res
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ export async function getServerSideProps(context: any) {
|
|||||||
where: {
|
where: {
|
||||||
documentId: recipient.Document.id,
|
documentId: recipient.Document.id,
|
||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
type: { in: [FieldType.SIGNATURE] },
|
|
||||||
Signature: { is: null },
|
Signature: { is: null },
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Button, IconButton } from "@documenso/ui";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
const SignPage: NextPageWithLayout = (props: any) => {
|
const Signed: NextPageWithLayout = (props: any) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const allRecipientsSigned = props.document.Recipient?.every(
|
const allRecipientsSigned = props.document.Recipient?.every(
|
||||||
(r: any) => r.signingStatus === "SIGNED"
|
(r: any) => r.signingStatus === "SIGNED"
|
||||||
@@ -47,7 +47,12 @@ const SignPage: NextPageWithLayout = (props: any) => {
|
|||||||
onClick={(event: any) => {
|
onClick={(event: any) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
router.push("/api/documents/" + props.document.id);
|
router.push(
|
||||||
|
"/api/documents/" +
|
||||||
|
props.document.id +
|
||||||
|
"?token=" +
|
||||||
|
props.recipient.token
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Download "{props.document.title}"
|
Download "{props.document.title}"
|
||||||
@@ -103,8 +108,9 @@ export async function getServerSideProps(context: any) {
|
|||||||
props: {
|
props: {
|
||||||
document: JSON.parse(JSON.stringify(recipient.Document)),
|
document: JSON.parse(JSON.stringify(recipient.Document)),
|
||||||
fields: JSON.parse(JSON.stringify(fields)),
|
fields: JSON.parse(JSON.stringify(fields)),
|
||||||
|
recipient: JSON.parse(JSON.stringify(recipient)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SignPage;
|
export default Signed;
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Login from "../components/login";
|
import Login from "../components/login";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage(props: any) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Login | Documenso</title>
|
<title>Login | Documenso</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Login></Login>
|
<Login allowSignup={props.ALLOW_SIGNUP}></Login>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getServerSideProps(context: any) {
|
||||||
|
const ALLOW_SIGNUP = process.env.ALLOW_SIGNUP === "true";
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
ALLOW_SIGNUP: ALLOW_SIGNUP,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ export default function SignupPage(props: { source: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps(context: any) {
|
export async function getServerSideProps(context: any) {
|
||||||
|
if (process.env.ALLOW_SIGNUP !== "true")
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/login",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const signupSource: string = context.query["source"];
|
const signupSource: string = context.query["source"];
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
BIN
apps/web/ressources/example.pdf
Normal file
BIN
apps/web/ressources/example.pdf
Normal file
Binary file not shown.
1908
package-lock.json
generated
1908
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",
|
"dev": "cd apps && cd web && next dev",
|
||||||
"build": "npm i && cd apps && cd web && npm i && next build",
|
"build": "npm i && cd apps && cd web && npm i && next build",
|
||||||
"start": "cd apps && cd web && next start",
|
"start": "cd apps && cd web && next start",
|
||||||
|
"db-migrate:dev": "prisma migrate dev",
|
||||||
"db-seed": "prisma db seed",
|
"db-seed": "prisma db seed",
|
||||||
"db-studio": "prisma studio"
|
"db-studio": "prisma studio"
|
||||||
},
|
},
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/node": "18.11.9",
|
"@types/node": "18.11.9",
|
||||||
"@types/react-dom": "18.0.9",
|
"@types/react-dom": "18.0.9",
|
||||||
|
"@types/react-signature-canvas": "^1.0.2",
|
||||||
"avatar-from-initials": "^1.0.3",
|
"avatar-from-initials": "^1.0.3",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
@@ -31,7 +33,7 @@
|
|||||||
"eslint-config-next": "13.0.3",
|
"eslint-config-next": "13.0.3",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"next": "13.0.3",
|
"next": "13.0.3",
|
||||||
"next-auth": "^4.18.3",
|
"next-auth": ">=4.20.1",
|
||||||
"next-transpile-modules": "^10.0.0",
|
"next-transpile-modules": "^10.0.0",
|
||||||
"npm": "^9.1.3",
|
"npm": "^9.1.3",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
@@ -41,9 +43,5 @@
|
|||||||
"react-hot-toast": "^2.4.0",
|
"react-hot-toast": "^2.4.0",
|
||||||
"react-signature-canvas": "^1.0.6",
|
"react-signature-canvas": "^1.0.6",
|
||||||
"typescript": "4.8.4"
|
"typescript": "4.8.4"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react-signature-canvas": "^1.0.2",
|
|
||||||
"file-loader": "^6.2.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ export const uploadDocument = async (event: any) => {
|
|||||||
if (event.target.files && event.target.files[0]) {
|
if (event.target.files && event.target.files[0]) {
|
||||||
const body = new FormData();
|
const body = new FormData();
|
||||||
const document = event.target.files[0];
|
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 || "");
|
body.append("document", document || "");
|
||||||
const response: any = await toast
|
const response: any = await toast
|
||||||
.promise(
|
.promise(
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import toast from "react-hot-toast";
|
|||||||
|
|
||||||
export const createOrUpdateField = async (
|
export const createOrUpdateField = async (
|
||||||
document: any,
|
document: any,
|
||||||
field: any
|
field: any,
|
||||||
|
recipientToken: string = ""
|
||||||
): Promise<any> => {
|
): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
const created = await toast.promise(
|
const created = await toast.promise(
|
||||||
fetch("/api/documents/" + document.id + "/fields", {
|
fetch("/api/documents/" + document.id + "/fields?token=" + recipientToken, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -11,14 +11,29 @@ export const sendMail = async (
|
|||||||
content: string | Buffer;
|
content: string | Buffer;
|
||||||
}[] = []
|
}[] = []
|
||||||
) => {
|
) => {
|
||||||
if (!process.env.SENDGRID_API_KEY)
|
let transport;
|
||||||
throw new Error("Sendgrid API Key not set.");
|
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
|
await transport
|
||||||
.sendMail({
|
.sendMail({
|
||||||
from: process.env.MAIL_FROM,
|
from: process.env.MAIL_FROM,
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import { SendStatus, ReadStatus, DocumentStatus } from "@prisma/client";
|
|||||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||||
import { signingRequestTemplate } from "@documenso/lib/mail";
|
import { signingRequestTemplate } from "@documenso/lib/mail";
|
||||||
|
|
||||||
export const sendSigningRequest = async (
|
export const sendSigningRequest = async (recipient: any, document: any, user: any) => {
|
||||||
recipient: any,
|
const signingRequestMessage = user.name
|
||||||
document: any,
|
? `${user.name} (${user.email}) has sent you a document to sign. `
|
||||||
user: any
|
: `${user.email} has sent you a document to sign. `;
|
||||||
) => {
|
|
||||||
await sendMail(
|
await sendMail(
|
||||||
recipient.email,
|
recipient.email,
|
||||||
`Please sign ${document.title}`,
|
`Please sign ${document.title}`,
|
||||||
signingRequestTemplate(
|
signingRequestTemplate(
|
||||||
`${user.name} (${user.email}) has sent you a document to sign. `,
|
signingRequestMessage,
|
||||||
document,
|
document,
|
||||||
recipient,
|
recipient,
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${document.id}/sign?token=${recipient.token}`,
|
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${document.id}/sign?token=${recipient.token}`,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export async function getUserFromToken(
|
|||||||
const tokenEmail = token?.email?.toString();
|
const tokenEmail = token?.email?.toString();
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
res.status(401).send("No session token found for request.");
|
if (res.status) res.status(401).send("No session token found for request.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
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"
|
||||||
File diff suppressed because one or more lines are too long
105
packages/ui/components/dialog/Dialog.tsx
Normal file
105
packages/ui/components/dialog/Dialog.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Transition, Dialog as DialogComponent } from "@headlessui/react";
|
||||||
|
import { Fragment } from "react";
|
||||||
|
import { Button } from "@documenso/ui";
|
||||||
|
import { sendSigningRequests } from "@documenso/lib/api";
|
||||||
|
import { Document as PrismaDocument } from "@prisma/client";
|
||||||
|
|
||||||
|
type FormValue = {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DialogProps = {
|
||||||
|
title: string;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
document: PrismaDocument;
|
||||||
|
formValues: FormValue[];
|
||||||
|
setLoading: (loading: boolean) => void;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Dialog({
|
||||||
|
title,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
document,
|
||||||
|
formValues,
|
||||||
|
setLoading,
|
||||||
|
icon,
|
||||||
|
}: DialogProps) {
|
||||||
|
const unsentEmailsLength = formValues.filter(
|
||||||
|
(s: any) => s.email && s.sendStatus != "SENT"
|
||||||
|
).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={open} as={Fragment}>
|
||||||
|
<DialogComponent 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 transition-opacity bg-gray-500 bg-opacity-75" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div className="flex items-end justify-center min-h-full 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"
|
||||||
|
>
|
||||||
|
<DialogComponent.Panel className="relative px-4 pt-5 pb-4 overflow-hidden text-left transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-green-100 rounded-full">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-center sm:mt-5">
|
||||||
|
<DialogComponent.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-medium leading-6 text-gray-900"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</DialogComponent.Title>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{`"${document.title}" will be sent to ${unsentEmailsLength} recipients.`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3 mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:flex-none ">
|
||||||
|
<Button color="secondary" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
setLoading(true);
|
||||||
|
sendSigningRequests(document).finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogComponent.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogComponent>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
packages/ui/components/dialog/index.ts
Normal file
1
packages/ui/components/dialog/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Dialog } from "./Dialog";
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export { Button, IconButton } from "./components/button/index";
|
export { Button, IconButton } from "./components/button/index";
|
||||||
export { SelectBox } from "./components/selectBox/index";
|
export { SelectBox } from "./components/selectBox/index";
|
||||||
export { Breadcrumb } from "./components/breadcrumb/index";
|
export { Breadcrumb } from "./components/breadcrumb/index";
|
||||||
|
export { Dialog } from "./components/dialog/index";
|
||||||
|
|||||||
Reference in New Issue
Block a user