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
|
||||
# You use the provided remote test database, courtesy of the documenso team: postgres://documenso_test_user:GnmLG14u12sd9zHsd4vVWwP40WneFJMo@dpg-cf2hljh4reb5o45oqpq0-a.oregon-postgres.render.com/documenso_test_e2i3
|
||||
# It is however recommend, that you set up a local Postgres SQL instance
|
||||
# ⚠ WARNING: The test database can be resetted or taken offline at any point
|
||||
# Option 1: You can use the provided remote test database, courtesy of the documenso team: postgres://documenso_test_user:GnmLG14u12sd9zHsd4vVWwP40WneFJMo@dpg-cf2hljh4reb5o45oqpq0-a.oregon-postgres.render.com/documenso_test_e2i3
|
||||
# Option 2: Set up a local Postgres SQL instance (RECOMMENDED)
|
||||
# ⚠ WARNING: The test database can be resetted or taken offline at any point.
|
||||
# ⚠ WARNING: Please be aware that nothing written to the test databae is private.
|
||||
DATABASE_URL=''
|
||||
|
||||
@@ -13,9 +13,21 @@ NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
|
||||
NEXTAUTH_SECRET='lorem ipsum sit dolor random string for encryption this could literally be anything'
|
||||
NEXTAUTH_URL='http://localhost:3000'
|
||||
|
||||
# MAIL
|
||||
# MAIL (NODEMAILER)
|
||||
# SENDGRID
|
||||
# Get a Sendgrid Api key here: https://signup.sendgrid.com
|
||||
# You can also configure you own SMTP server using Nodemailer in sendMailts. (currently not possible via config)
|
||||
SENDGRID_API_KEY=''
|
||||
|
||||
# SMTP
|
||||
# Set SMTP credentials to use SMTP instead of the Sendgrid API.
|
||||
SMTP_MAIL_HOST=''
|
||||
SMTP_MAIL_PORT=''
|
||||
SMTP_MAIL_USER=''
|
||||
SMTP_MAIL_PASSWORD=''
|
||||
|
||||
# Sender for signing requests and completion mails.
|
||||
MAIL_FROM=''
|
||||
MAIL_FROM='documenso@localhost.com'
|
||||
|
||||
#FEATURE FLAGS
|
||||
# Allow users to register via the /signup page. Otherwise they will be redirect to the home page.
|
||||
ALLOW_SIGNUP=true
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
||||
[submodule "apps/website/documenso/website"]
|
||||
path = apps/website/documenso/website
|
||||
url = http://github.com/eltimuro/website.git
|
||||
url = http://github.com/documenso/website.git
|
||||
@@ -1,31 +1,37 @@
|
||||
|
||||
# Contributing to Documenso
|
||||
|
||||
If you plan to contribute to Documenso, please take a moment to feel awesome ✨ People like you are what open source is about ♥. Any contributions, no matter how big or small, are highly appreciated.
|
||||
|
||||
## Before getting started
|
||||
|
||||
- Before jumping into a PR be sure to search [existing PRs](https://github.com/documenso/documenso/pulls) or [issues](https://github.com/documenso/documenso/issues) for an open or closed item that relates to your submission.
|
||||
- Select and issue from [here](https://github.com/documenso/documenso/issues) or create a new one
|
||||
- Consider the results from the discussion in the issue
|
||||
|
||||
## Developing
|
||||
The development branch is <code>development</code>. All pull request should be made against this branch. If you need help getting started, [join us on Slack](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w).
|
||||
|
||||
The development branch is <code>main</code>. All pull request should be made against this branch. If you need help getting started, [join us on Slack](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w).
|
||||
|
||||
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
|
||||
own GitHub account and then
|
||||
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||
2. Create a new branch:
|
||||
|
||||
- Create a new branch (include the issue id and somthing readable):
|
||||
|
||||
```sh
|
||||
git checkout -b doc-999-my-feature-or-fix
|
||||
```
|
||||
```sh
|
||||
git checkout -b doc-999-my-feature-or-fix
|
||||
```
|
||||
|
||||
3. See the [Developer Setup](https://github.com/documenso/documenso/blob/main/README.md#developer-setup) for more setup details.
|
||||
## Building
|
||||
|
||||
## Building
|
||||
|
||||
> **Note**
|
||||
> Please be sure that you can make a full production build before pushing code or creating PRs.
|
||||
|
||||
You can build the project with:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
> **Note**
|
||||
> Please be sure that you can make a full production build before pushing code or creating PRs.
|
||||
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://github.com/documenso/documenso/stargazers"><img src="https://img.shields.io/github/stars/documenso/documenso" alt="Github Stars"></a>
|
||||
<a href="https://github.com/documenso/documenso/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
||||
<a href="https://github.com/documenso/documensom/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
|
||||
<a href="https://github.com/documenso/documenso/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
|
||||
</p>
|
||||
|
||||
# Documenso 0.9 - Developer Preview
|
||||
@@ -57,6 +57,7 @@
|
||||
Signing documents digitally is fast, easy and should be best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure.
|
||||
|
||||
## Community and Next Steps 🎯
|
||||
|
||||
The current project goal is to <b>[release a production ready version](https://github.com/documenso/documenso/milestone/1)</b> for self-hosting as soon as possible. If you want to help making that happen you can:
|
||||
|
||||
- Check out the first source code release in this repository and test it
|
||||
@@ -67,13 +68,11 @@ The current project goal is to <b>[release a production ready version](https://g
|
||||
- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release
|
||||
|
||||
## Contributing
|
||||
|
||||
- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Tools
|
||||
|
||||
- This repos uses 📝 https://gitmoji.dev/ for more expressive commit messages.
|
||||
- Use 🧹 for quality of code (eg remove comments, debug output, remove unused code)
|
||||
|
||||
# Tech
|
||||
|
||||
Documenso is built using awesome open source tech including:
|
||||
@@ -105,32 +104,37 @@ To run Documenso locally you need
|
||||
Follow these steps to setup documenso on you local machnine:
|
||||
|
||||
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||
```sh
|
||||
git clone https://github.com/documenso/documenso
|
||||
```
|
||||
```sh
|
||||
git clone https://github.com/documenso/documenso
|
||||
```
|
||||
- Run <code>npm i</code> in root directory
|
||||
- Rename .env.example to .env
|
||||
- Rename <code>.env.example</code> to <code>.env</code>
|
||||
- Set DATABASE_URL value in .env file
|
||||
- You can use the provided test database url (may be wiped at any point)
|
||||
- Or setup a local postgres sql instance (recommened)
|
||||
- Set SENDGRID_API_KEY value in .env file
|
||||
- You need SendGrid account, which you can create [here](https://signup.sendgrid.com/).
|
||||
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own smtp server
|
||||
- Create the database scheme by running <code>db-migrate:dev</code>
|
||||
- Setup your mail provider
|
||||
- Set <code>SENDGRID_API_KEY</code> value in .env file
|
||||
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/).
|
||||
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the <code>SMTP\_\* varibles</code> in your .env
|
||||
- Run <code>npm run dev</code> root directory to start
|
||||
- Register a new user at http://localhost:3000/signup
|
||||
|
||||
---
|
||||
- Optional: Seed the database using <code>npm run db-seed</code> to create a test user and document
|
||||
- Optional: Upload and sign <code>apps\web\ressources\example.pdf</code> manually to test your setup
|
||||
|
||||
- Optional: Create your own signing certificate
|
||||
- A demo certificate is provided in /app/web/ressources/certificate.p12
|
||||
- To generate you own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signging certificate**.
|
||||
|
||||
## Updating
|
||||
|
||||
- If you pull the newest version from main, using <code>git pull</code>, it may be neccessary to regenerate your database client
|
||||
- You can do this by running the generate command in /packages/prisma:
|
||||
```sh
|
||||
npx prisma generate
|
||||
```
|
||||
```sh
|
||||
npx prisma generate
|
||||
```
|
||||
- This is not neccessary on first clone
|
||||
|
||||
# Creating your own signging certificate
|
||||
@@ -144,7 +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> \
|
||||
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid.
|
||||
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this: \
|
||||
<code>openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt</code>
|
||||
<code>openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt</code>
|
||||
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
|
||||
5. Place the certificate <code>/apps/web/ressource/certificate.p12</code>
|
||||
|
||||
|
||||
@@ -25,9 +25,6 @@ export default function FieldTypeSelector(props: any) {
|
||||
onChange={(e: any) => {
|
||||
setSelectedFieldType(e);
|
||||
}}
|
||||
onMouseDown={(e: any) => {
|
||||
if (e.button === 0) props.setAdding(true);
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{fieldTypes.map((fieldType) => (
|
||||
|
||||
@@ -6,6 +6,8 @@ import { createOrUpdateField, deleteField } from "@documenso/lib/api";
|
||||
import { createField } from "@documenso/features/editor";
|
||||
import RecipientSelector from "./recipient-selector";
|
||||
import FieldTypeSelector from "./field-type-selector";
|
||||
import { InformationCircleIcon } from "@heroicons/react/24/outline";
|
||||
import Link from "next/link";
|
||||
const stc = require("string-to-color");
|
||||
|
||||
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 [selectedRecipient, setSelectedRecipient]: any = useState();
|
||||
const [selectedFieldType, setSelectedFieldType] = useState();
|
||||
const noRecipients = props?.document.Recipient.length === 0;
|
||||
const [adding, setAdding] = useState(false);
|
||||
const noRecipients =
|
||||
props?.document.Recipient.length === 0 ||
|
||||
props?.document.Recipient.every((e: any) => !e.email);
|
||||
|
||||
function onPositionChangedHandler(position: any, id: any) {
|
||||
if (!position) return;
|
||||
@@ -47,9 +50,41 @@ export default function PDFEditor(props: any) {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div hidden={!noRecipients} className="rounded-md bg-yellow-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<InformationCircleIcon
|
||||
className="h-5 w-5 text-yellow-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p className="text-sm text-yellow-700">
|
||||
This document does not have any recipients. Add recipients to
|
||||
create fields.
|
||||
</p>
|
||||
<p className="mt-3 text-sm md:mt-0 md:ml-6">
|
||||
<Link
|
||||
href={
|
||||
NEXT_PUBLIC_WEBAPP_URL +
|
||||
"/documents/" +
|
||||
props.document.id +
|
||||
"/recipients"
|
||||
}
|
||||
className="whitespace-nowrap font-medium text-yellow-700 hover:text-yellow-600"
|
||||
>
|
||||
Add Recipients
|
||||
<span aria-hidden="true"> →</span>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PDFViewer
|
||||
style={{
|
||||
cursor: `url("https://place-hold.it/110x64/37f095/FFFFFF&text=${selectedFieldType}") 55 32, auto`,
|
||||
cursor: !noRecipients
|
||||
? `url("https://place-hold.it/110x64/37f095/FFFFFF&text=${selectedFieldType}") 55 32, auto`
|
||||
: "",
|
||||
}}
|
||||
readonly={false}
|
||||
document={props.document}
|
||||
@@ -60,11 +95,6 @@ export default function PDFEditor(props: any) {
|
||||
onMouseUp={(e: any, page: number) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log(adding);
|
||||
if (adding) {
|
||||
addField(e, page);
|
||||
setAdding(false);
|
||||
}
|
||||
}}
|
||||
onMouseDown={(e: any, page: number) => {
|
||||
if (e.button === 0) addField(e, page);
|
||||
@@ -80,7 +110,6 @@ export default function PDFEditor(props: any) {
|
||||
/>
|
||||
<hr className="m-3 border-slate-300"></hr>
|
||||
<FieldTypeSelector
|
||||
setAdding={setAdding}
|
||||
selectedRecipient={selectedRecipient}
|
||||
onChange={setSelectedFieldType}
|
||||
/>
|
||||
@@ -92,6 +121,7 @@ export default function PDFEditor(props: any) {
|
||||
function addField(e: any, page: number) {
|
||||
if (!selectedRecipient) return;
|
||||
if (!selectedFieldType) return;
|
||||
if (noRecipients) return;
|
||||
|
||||
const signatureField = createField(
|
||||
e,
|
||||
@@ -101,7 +131,7 @@ export default function PDFEditor(props: any) {
|
||||
);
|
||||
|
||||
createOrUpdateField(props?.document, signatureField).then((res) => {
|
||||
setFields(fields.concat(res));
|
||||
setFields((prevState) => [...prevState, res]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
CheckBadgeIcon,
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import toast from "react-hot-toast";
|
||||
import { FieldType } from "@prisma/client";
|
||||
import {
|
||||
createOrUpdateField,
|
||||
@@ -70,7 +69,7 @@ export default function PDFSigner(props: any) {
|
||||
);
|
||||
const signedField = { ...dialogField };
|
||||
signedField.signature = signature;
|
||||
setFields(fields.concat(signedField));
|
||||
setFields((prevState) => [...prevState, signedField]);
|
||||
setOpen(false);
|
||||
setDialogField(null);
|
||||
}
|
||||
@@ -174,8 +173,12 @@ export default function PDFSigner(props: any) {
|
||||
FieldType.FREE_SIGNATURE
|
||||
);
|
||||
|
||||
createOrUpdateField(props.document, freeSignatureField).then((res) => {
|
||||
setFields(fields.concat(res));
|
||||
createOrUpdateField(
|
||||
props.document,
|
||||
freeSignatureField,
|
||||
recipient.token
|
||||
).then((res) => {
|
||||
setFields((prevState) => [...prevState, res]);
|
||||
setDialogField(res);
|
||||
setOpen(true);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
|
||||
import EditableField from "./editable-field";
|
||||
import SignableField from "./signable-field";
|
||||
import short from "short-uuid";
|
||||
import { FieldType } from "@prisma/client";
|
||||
|
||||
export default function PDFViewer(props) {
|
||||
const [numPages, setNumPages] = useState(null);
|
||||
@@ -71,21 +72,25 @@ export default function PDFViewer(props) {
|
||||
onRenderError={() => setLoading(false)}
|
||||
></Page>
|
||||
{props?.fields
|
||||
.filter((item) => item.page === index)
|
||||
.map((item) =>
|
||||
.filter((field) => field.page === index)
|
||||
.map((field) =>
|
||||
props.readonly ? (
|
||||
<SignableField
|
||||
onClick={props.onClick}
|
||||
key={item.id}
|
||||
field={item}
|
||||
key={field.id}
|
||||
field={field}
|
||||
className="absolute"
|
||||
onDelete={onDeleteHandler}
|
||||
></SignableField>
|
||||
) : (
|
||||
<EditableField
|
||||
hidden={item.Signature || item.inserted}
|
||||
key={item.id}
|
||||
field={item}
|
||||
hidden={
|
||||
field.Signature ||
|
||||
field.inserted ||
|
||||
field.type === FieldType.FREE_SIGNATURE
|
||||
}
|
||||
key={field.id}
|
||||
field={field}
|
||||
className="absolute"
|
||||
onPositionChanged={onPositionChangedHandler}
|
||||
onDelete={onDeleteHandler}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState } from "react";
|
||||
import Draggable from "react-draggable";
|
||||
import { IconButton } from "@documenso/ui";
|
||||
import { XCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { classNames } from "@documenso/lib";
|
||||
const stc = require("string-to-color");
|
||||
|
||||
type FieldPropsType = {
|
||||
@@ -34,22 +35,28 @@ export default function SignableField(props: FieldPropsType) {
|
||||
defaultPosition={{ x: 0, y: 0 }}
|
||||
cancel="div"
|
||||
onMouseDown={(e: any) => {
|
||||
e.preventDefault();
|
||||
// e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onClick={(e: any) => {
|
||||
if (!field?.signature) props.onClick(props.field);
|
||||
}}
|
||||
ref={nodeRef}
|
||||
className="cursor-pointer opacity-80 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none hover:brightness-50"
|
||||
className={classNames(
|
||||
"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={{
|
||||
background: stc(props.field.Recipient.email),
|
||||
}}
|
||||
>
|
||||
<div hidden={field?.signature} className="font-medium my-4">
|
||||
{field.type === "SIGNATURE" ? "SIGN HERE" : ""}
|
||||
{field.type === "DATE" ? <small>Date (filled on sign)</small> : ""}
|
||||
</div>
|
||||
<div
|
||||
hidden={!field?.signature}
|
||||
|
||||
@@ -17,12 +17,11 @@ interface LoginValues {
|
||||
csrfToken: string;
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
export default function Login(props: any) {
|
||||
const router = useRouter();
|
||||
const methods = useForm<LoginValues>();
|
||||
const { register, formState } = methods;
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
let callbackUrl =
|
||||
typeof router.query?.callbackUrl === "string"
|
||||
? router.query.callbackUrl
|
||||
@@ -117,7 +116,6 @@ export default function Login() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm">
|
||||
<a href="#" className="font-medium text-neon hover:text-neon">
|
||||
@@ -125,7 +123,6 @@ export default function Login() {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -152,15 +149,27 @@ export default function Login() {
|
||||
<div className="relative flex justify-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Are you new here?{" "}
|
||||
<Link
|
||||
href="/signup"
|
||||
className="font-medium text-neon hover:text-neon"
|
||||
>
|
||||
Create a new Account
|
||||
</Link>
|
||||
</p>
|
||||
{props.allowSignup ? (
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Are you new here?{" "}
|
||||
<Link
|
||||
href="/signup"
|
||||
className="font-medium text-neon hover:text-neon"
|
||||
>
|
||||
Create a new Account
|
||||
</Link>
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Like Documenso{" "}
|
||||
<Link
|
||||
href="https://documenso.com"
|
||||
className="font-medium text-neon hover:text-neon"
|
||||
>
|
||||
Hosted Documenso will be availible soon™
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
|
||||
@@ -34,13 +34,18 @@ const navigation = [
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
href: "/settings",
|
||||
href: "/settings/profile",
|
||||
current: true,
|
||||
icon: WrenchIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const userNavigation = [
|
||||
{ name: "Your Profile", href: "/settings/profile", icon: UserCircleIcon },
|
||||
{
|
||||
name: "Your Profile",
|
||||
href: "/settings/profile",
|
||||
icon: UserCircleIcon,
|
||||
},
|
||||
{
|
||||
name: "Sign out",
|
||||
href: "",
|
||||
@@ -95,13 +100,15 @@ export default function TopNavigation() {
|
||||
}, [session]);
|
||||
|
||||
navigation.forEach((element) => {
|
||||
element.current = router.route.endsWith("/" + element.href.split("/")[1]);
|
||||
element.current =
|
||||
router.route.endsWith("/" + element.href.split("/")[1]) ||
|
||||
router.route.includes(element.href.split("/")[1]);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Disclosure as="nav" className="border-b border-gray-200 bg-white">
|
||||
{({ open }) => (
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 justify-between">
|
||||
@@ -151,10 +158,7 @@ export default function TopNavigation() {
|
||||
<div
|
||||
key={user?.email}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: avatarFromInitials(
|
||||
user?.name || "" || "",
|
||||
40
|
||||
),
|
||||
__html: avatarFromInitials(user?.name || "" || "", 40),
|
||||
}}
|
||||
/>
|
||||
</Menu.Button>
|
||||
@@ -220,6 +224,9 @@ export default function TopNavigation() {
|
||||
"block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
|
||||
)}
|
||||
aria-current={item.current ? "page" : undefined}
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
@@ -236,19 +243,21 @@ export default function TopNavigation() {
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<div className="text-base font-medium text-gray-800">
|
||||
{user?.name || ""}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-500">
|
||||
{user?.email}
|
||||
</div>
|
||||
<div className="text-base font-medium text-gray-800">{user?.name || ""}</div>
|
||||
<div className="text-sm font-medium text-gray-500">{user?.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 space-y-1">
|
||||
{userNavigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
onClick={item.click}
|
||||
onClick={
|
||||
item.href.includes("/settings/profile")
|
||||
? () => {
|
||||
close();
|
||||
}
|
||||
: item.click
|
||||
}
|
||||
href={item.href}
|
||||
className="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800"
|
||||
>
|
||||
|
||||
@@ -115,9 +115,13 @@ export default function Setttings() {
|
||||
</aside>
|
||||
|
||||
<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="#"
|
||||
method="POST"
|
||||
hidden={
|
||||
subNavigation.filter((e) => e.current)[0]?.name !==
|
||||
subNavigation[0].name
|
||||
}
|
||||
>
|
||||
{/* Profile section */}
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
@@ -163,13 +167,33 @@ export default function Setttings() {
|
||||
name="first-name"
|
||||
id="first-name"
|
||||
autoComplete="given-name"
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
|
||||
className="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>
|
||||
<Button onClick={() => updateUser(user)}>Save</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
hidden={
|
||||
subNavigation.filter((e) => e.current)[0]?.name !==
|
||||
subNavigation[1].name
|
||||
}
|
||||
className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
|
||||
>
|
||||
{/* 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>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"formidable": "^3.2.5",
|
||||
"install": "^0.13.0",
|
||||
"next": "13.0.3",
|
||||
"next-auth": "^4.18.3",
|
||||
"next-auth": ">=4.20.1",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-signpdf": "^1.5.0",
|
||||
|
||||
@@ -18,10 +18,10 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
}
|
||||
|
||||
let user = null;
|
||||
|
||||
let recipient = null;
|
||||
if (recipientToken) {
|
||||
// Request from signing page without login
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
token: recipientToken?.toString(),
|
||||
},
|
||||
@@ -37,7 +37,14 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
if (!user) return res.status(401).end();
|
||||
|
||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||
let document: PrismaDocument | null = null;
|
||||
if (recipientToken) {
|
||||
document = await prisma.document.findFirst({
|
||||
where: { id: recipient?.Document?.id },
|
||||
});
|
||||
} else {
|
||||
document = await getDocument(+documentId, req, res);
|
||||
}
|
||||
|
||||
if (!document)
|
||||
res.status(404).end(`No document with id ${documentId} found.`);
|
||||
@@ -45,16 +52,18 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const signaturesCount = await prisma.signature.count({
|
||||
where: {
|
||||
Field: {
|
||||
documentId: document.id,
|
||||
documentId: document?.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let signedDocumentAsBase64 = document.document;
|
||||
let signedDocumentAsBase64 = document?.document || "";
|
||||
|
||||
// No need to add a signature, if no one signed yet.
|
||||
if (signaturesCount > 0) {
|
||||
signedDocumentAsBase64 = await addDigitalSignature(document.document);
|
||||
signedDocumentAsBase64 = await addDigitalSignature(
|
||||
document?.document || ""
|
||||
);
|
||||
}
|
||||
|
||||
const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64");
|
||||
@@ -62,7 +71,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
res.setHeader("Content-Length", buffer.length);
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`attachment; filename=${document.title}`
|
||||
`attachment; filename=${document?.title}`
|
||||
);
|
||||
|
||||
return res.status(200).send(buffer);
|
||||
|
||||
@@ -36,8 +36,10 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
}
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
const { id: documentId } = req.query;
|
||||
const { token: recipientToken } = req.query;
|
||||
let user = null;
|
||||
if (!recipientToken) user = await getUserFromToken(req, res);
|
||||
if (!user && !recipientToken) return res.status(401).end();
|
||||
const body: {
|
||||
id: number;
|
||||
type: FieldType;
|
||||
@@ -48,18 +50,30 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
customText: string;
|
||||
} = req.body;
|
||||
|
||||
if (!user) return;
|
||||
|
||||
const { id: documentId } = req.query;
|
||||
if (!documentId) {
|
||||
res.status(400).send("Missing parameter documentId.");
|
||||
return;
|
||||
return res.status(400).send("Missing parameter documentId.");
|
||||
}
|
||||
|
||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||
if (recipientToken) {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: { token: recipientToken?.toString() },
|
||||
});
|
||||
|
||||
// todo entity ownerships checks
|
||||
if (document.userId !== user.id) {
|
||||
return res.status(401).send("User does not have access to this document.");
|
||||
if (!recipient || recipient?.documentId !== +documentId)
|
||||
return res
|
||||
.status(401)
|
||||
.send("Recipient does not have access to this document.");
|
||||
}
|
||||
|
||||
if (user) {
|
||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||
// todo entity ownerships checks
|
||||
if (document.userId !== user.id) {
|
||||
return res
|
||||
.status(401)
|
||||
.send("User does not have access to this document.");
|
||||
}
|
||||
}
|
||||
|
||||
const field = await prisma.field.upsert({
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
defaultHandler,
|
||||
defaultResponder,
|
||||
getUserFromToken,
|
||||
} from "@documenso/lib/server";
|
||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { SigningStatus, DocumentStatus } from "@prisma/client";
|
||||
@@ -12,7 +8,6 @@ import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
|
||||
import { sendSigningDoneMail } from "@documenso/lib/mail";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const existingUser = await getUserFromToken(req, res);
|
||||
const { token: recipientToken } = req.query;
|
||||
const { signatures: signaturesFromBody }: { signatures: any[] } = req.body;
|
||||
|
||||
@@ -29,11 +24,19 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
return res.status(401).send("Recipient not found.");
|
||||
}
|
||||
|
||||
const document: PrismaDocument = await getDocument(
|
||||
recipient.documentId,
|
||||
req,
|
||||
res
|
||||
);
|
||||
const document: PrismaDocument = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: recipient.documentId,
|
||||
},
|
||||
include: {
|
||||
Recipient: {
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
},
|
||||
Field: { include: { Recipient: true, Signature: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) res.status(404).end(`No document found.`);
|
||||
|
||||
@@ -70,6 +73,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({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
defaultHandler,
|
||||
defaultResponder,
|
||||
getUserFromToken,
|
||||
} from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
|
||||
import { truncate } from "fs";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
|
||||
type FormValues = {
|
||||
document: File;
|
||||
@@ -58,18 +59,18 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
Dashboard
|
||||
</h1>
|
||||
</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) => (
|
||||
<Link href={item.link} key={item.name}>
|
||||
<div className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6 ">
|
||||
<dt className="truncate text-sm font-medium text-gray-500 ">
|
||||
<div className="px-4 py-3 overflow-hidden bg-white rounded-lg shadow md:p-6 sm:py-5">
|
||||
<dt className="text-sm font-medium text-gray-500 truncate ">
|
||||
<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"
|
||||
></item.icon>
|
||||
{item.name}
|
||||
</dt>
|
||||
<dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900">
|
||||
<dd className="mt-1 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
|
||||
{getStat(item.name, props)}
|
||||
</dd>
|
||||
</div>
|
||||
@@ -80,6 +81,7 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
<input
|
||||
id="fileUploadHelper"
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
onChange={(event: any) => {
|
||||
uploadDocument(event);
|
||||
}}
|
||||
@@ -90,10 +92,10 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
onClick={() => {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}}
|
||||
className="cursor-pointer relative block w-full rounded-lg border-2 border-dashed border-gray-300 p-12 text-center hover:border-neon focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
className="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
|
||||
className="mx-auto h-12 w-12 text-gray-400"
|
||||
className="w-12 h-12 mx-auto text-gray-400"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span className="mt-2 block text-sm font-medium text-neon">
|
||||
Upload a new PDF document
|
||||
<span
|
||||
id="add_document"
|
||||
className="mt-2 block text-sm font-medium text-neon"
|
||||
>
|
||||
Add a new PDF document.
|
||||
</span>
|
||||
</div>
|
||||
<ReactTooltip
|
||||
anchorId="add_document"
|
||||
place="bottom"
|
||||
content="No preparation needed. Any PDF will do."
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,10 +16,10 @@ import {
|
||||
import { useRouter } from "next/router";
|
||||
import { uploadDocument } from "@documenso/features";
|
||||
import { DocumentStatus } from "@prisma/client";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
import { Button, IconButton, SelectBox } from "@documenso/ui";
|
||||
import { NextPageContext } from "next";
|
||||
import { deleteDocument, getDocuments } from "@documenso/lib/api";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
|
||||
const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
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>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Get started by creating a new document.
|
||||
Get started by adding a document. Any PDF will do.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
@@ -415,11 +415,12 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}}
|
||||
>
|
||||
Upload Document
|
||||
Add Document
|
||||
</Button>
|
||||
<input
|
||||
id="fileUploadHelper"
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
onChange={(event: any) => {
|
||||
uploadDocument(event);
|
||||
}}
|
||||
@@ -427,6 +428,11 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
/>
|
||||
</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 { Fragment, ReactElement, useRef, useState } from "react";
|
||||
import { ReactElement, useRef, useState } from "react";
|
||||
import Layout from "../../../components/layout";
|
||||
import { NextPageWithLayout } from "../../_app";
|
||||
import { classNames, NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
|
||||
@@ -7,37 +7,27 @@ import {
|
||||
ArrowDownTrayIcon,
|
||||
CheckBadgeIcon,
|
||||
CheckIcon,
|
||||
EnvelopeIcon,
|
||||
PaperAirplaneIcon,
|
||||
PencilSquareIcon,
|
||||
TrashIcon,
|
||||
UserPlusIcon,
|
||||
EnvelopeIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
import { Breadcrumb, Button, IconButton } from "@documenso/ui";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import {
|
||||
createOrUpdateRecipient,
|
||||
deleteRecipient,
|
||||
sendSigningRequests,
|
||||
} from "@documenso/lib/api";
|
||||
import {
|
||||
FormProvider,
|
||||
useFieldArray,
|
||||
useForm,
|
||||
useWatch,
|
||||
} from "react-hook-form";
|
||||
import { Document as PrismaDocument, DocumentStatus } from "@prisma/client";
|
||||
import { Breadcrumb, Button, Dialog, IconButton } from "@documenso/ui";
|
||||
import { createOrUpdateRecipient, deleteRecipient, sendSigningRequests } from "@documenso/lib/api";
|
||||
|
||||
type FormValues = {
|
||||
import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
export type FormValues = {
|
||||
signers: { id: number; email: string; name: string }[];
|
||||
};
|
||||
|
||||
const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
const title: string =
|
||||
`"` + props?.document?.title + `"` + "Recipients | Documenso";
|
||||
const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso";
|
||||
const breadcrumbItems = [
|
||||
{
|
||||
title: "Documents",
|
||||
@@ -49,11 +39,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
},
|
||||
{
|
||||
title: "Recipients",
|
||||
href:
|
||||
NEXT_PUBLIC_WEBAPP_URL +
|
||||
"/documents/" +
|
||||
props.document.id +
|
||||
"/recipients",
|
||||
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -85,25 +71,17 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<div className="mt-10">
|
||||
<div className="px-6 mt-10 sm:px-0">
|
||||
<div>
|
||||
<Breadcrumb document={props.document} items={breadcrumbItems} />
|
||||
</div>
|
||||
<div className="mt-2 md:flex md:items-center md:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<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">
|
||||
{props.document.title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-shrink-0 md:mt-0 md:ml-4">
|
||||
<Button
|
||||
icon={PencilSquareIcon}
|
||||
color="secondary"
|
||||
className="mr-2"
|
||||
href={breadcrumbItems[1].href}
|
||||
>
|
||||
Edit Document
|
||||
</Button>
|
||||
<div className="flex flex-shrink-0 mt-4 md:mt-0 md:ml-4">
|
||||
<Button
|
||||
icon={ArrowDownTrayIcon}
|
||||
color="secondary"
|
||||
@@ -112,6 +90,15 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
<Button
|
||||
icon={PencilSquareIcon}
|
||||
disabled={props.document.status === DocumentStatus.COMPLETED}
|
||||
color={props.document.status === DocumentStatus.COMPLETED ? "primary" : "secondary"}
|
||||
className="mr-2"
|
||||
href={breadcrumbItems[1].href}
|
||||
>
|
||||
Edit Document
|
||||
</Button>
|
||||
<Button
|
||||
className="min-w-[125px]"
|
||||
color="primary"
|
||||
@@ -122,8 +109,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
disabled={
|
||||
(formValues.length || 0) === 0 ||
|
||||
!formValues.some(
|
||||
(r: any) =>
|
||||
r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
|
||||
(r: any) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
|
||||
) ||
|
||||
loading
|
||||
}
|
||||
@@ -132,12 +118,10 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-md bg-white shadow mt-10 p-6">
|
||||
<div className="border-b border-gray-200 pb-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900">
|
||||
Signers
|
||||
</h3>
|
||||
<p className="mt-2 max-w-4xl text-sm text-gray-500">
|
||||
<div className="p-4 mt-10 overflow-hidden bg-white rounded-md shadow sm:p-6">
|
||||
<div className="pb-3 border-b border-gray-200 sm:pb-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900 ">Signers</h3>
|
||||
<p className="max-w-4xl mt-2 text-sm text-gray-500">
|
||||
The people who will sign the document.
|
||||
</p>
|
||||
</div>
|
||||
@@ -151,189 +135,174 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
{fields.map((item: any, index: number) => (
|
||||
<li
|
||||
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
|
||||
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"
|
||||
<div id="container" className="block w-full lg:flex lg:justify-between">
|
||||
<div className="block space-y-2 md:space-x-2 md:space-y-0 md:flex">
|
||||
<div
|
||||
className={classNames(
|
||||
"md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
|
||||
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
||||
)}
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
{...register(`signers.${index}.email`, {
|
||||
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
})}
|
||||
defaultValue={item.email}
|
||||
disabled={item.sendStatus === "SENT" || loading}
|
||||
onBlur={() => {
|
||||
if (!errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event: any) => {
|
||||
if (event.key === "Enter")
|
||||
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
{...register(`signers.${index}.email`, {
|
||||
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
})}
|
||||
defaultValue={item.email}
|
||||
disabled={item.sendStatus === "SENT" || loading}
|
||||
onBlur={() => {
|
||||
if (!errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 sm:text-sm outline-none bg-inherit"
|
||||
placeholder="john.dorian@loremipsum.com"
|
||||
/>
|
||||
{errors?.signers?.[index] ? (
|
||||
<p
|
||||
className="mt-2 text-sm text-red-600"
|
||||
id="email-error"
|
||||
>
|
||||
<XMarkIcon className="inline h-5" /> Invalid Email
|
||||
</p>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"ml-3 w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
|
||||
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
||||
)}
|
||||
>
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-xs font-medium text-gray-900"
|
||||
>
|
||||
Name (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register(`signers.${index}.name`)}
|
||||
defaultValue={item.name}
|
||||
disabled={item.sendStatus === "SENT" || loading}
|
||||
onBlur={() => {
|
||||
if (!errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event: any) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!errors?.signers?.[index]
|
||||
)
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 sm:text-sm outline-none bg-inherit"
|
||||
placeholder="John Dorian"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-auto flex">
|
||||
<div key={item.id}>
|
||||
{item.sendStatus === "NOT_SENT" ? (
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="inline-block mt-3 flex-shrink-0 rounded-full bg-gray-200 px-2 py-0.5 text-xs font-medium text-gray-800"
|
||||
>
|
||||
Not Sent
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{item.sendStatus === "SENT" &&
|
||||
item.readStatus !== "OPENED" ? (
|
||||
<span id="sent_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
|
||||
>
|
||||
<CheckIcon className="inline h-5 mr-1"></CheckIcon>{" "}
|
||||
Sent
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{item.readStatus === "OPENED" &&
|
||||
item.signingStatus === "NOT_SIGNED" ? (
|
||||
<span id="read_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
|
||||
>
|
||||
<CheckIcon className="inline h-5 -mr-2"></CheckIcon>
|
||||
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
|
||||
Seen
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{item.signingStatus === "SIGNED" ? (
|
||||
<span id="signed_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="inline-block mt-3 flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"
|
||||
>
|
||||
<CheckBadgeIcon className="inline h-5 mr-1"></CheckBadgeIcon>
|
||||
Signed
|
||||
</span>
|
||||
</span>
|
||||
}}
|
||||
onKeyDown={(event: any) => {
|
||||
if (event.key === "Enter")
|
||||
if (!errors?.signers?.[index])
|
||||
createOrUpdateRecipient({
|
||||
...formValues[index],
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
className="block w-full p-0 text-gray-900 placeholder-gray-500 disabled:bg-neutral-100 border-0 outline-none sm:text-sm bg-inherit"
|
||||
placeholder="john.dorian@loremipsum.com"
|
||||
/>
|
||||
{errors?.signers?.[index] ? (
|
||||
<p className="mt-2 text-sm text-red-600" id="email-error">
|
||||
<XMarkIcon className="inline h-5" /> Invalid Email
|
||||
</p>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto flex mr-1">
|
||||
<IconButton
|
||||
icon={PaperAirplaneIcon}
|
||||
disabled={
|
||||
!item.id ||
|
||||
item.sendStatus !== "SENT" ||
|
||||
item.signingStatus === "SIGNED" ||
|
||||
loading
|
||||
}
|
||||
color="secondary"
|
||||
className="mr-4 h-9 my-auto"
|
||||
onClick={() => {
|
||||
if (confirm("Resend this signing request?")) {
|
||||
setLoading(true);
|
||||
sendSigningRequests(props.document, [
|
||||
item.id,
|
||||
]).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}}
|
||||
<div
|
||||
className={classNames(
|
||||
"md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
|
||||
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
||||
)}
|
||||
>
|
||||
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"
|
||||
/>
|
||||
<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 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>
|
||||
</li>
|
||||
@@ -359,79 +328,16 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={setOpen}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
|
||||
<div>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
||||
<EnvelopeIcon
|
||||
className="h-6 w-6 text-green-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Ready to send
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{`"${props.document.title}" will be sent to ${
|
||||
formValues.filter(
|
||||
(s: any) => s.email && s.sendStatus != "SENT"
|
||||
).length
|
||||
} recipients.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
|
||||
<Button color="secondary" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
setLoading(true);
|
||||
sendSigningRequests(props.document).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<Dialog
|
||||
title="Ready to send"
|
||||
document={props.document}
|
||||
formValues={formValues}
|
||||
open={open}
|
||||
setLoading={setLoading}
|
||||
setOpen={setOpen}
|
||||
icon={<EnvelopeIcon className="w-6 h-6 text-green-600" aria-hidden="true" />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -451,11 +357,7 @@ export async function getServerSideProps(context: any) {
|
||||
};
|
||||
|
||||
const { id: documentId } = context.query;
|
||||
const document: PrismaDocument = await getDocument(
|
||||
+documentId,
|
||||
context.req,
|
||||
context.res
|
||||
);
|
||||
const document: PrismaDocument = await getDocument(+documentId, context.req, context.res);
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -107,7 +107,6 @@ export async function getServerSideProps(context: any) {
|
||||
where: {
|
||||
documentId: recipient.Document.id,
|
||||
recipientId: recipient.id,
|
||||
type: { in: [FieldType.SIGNATURE] },
|
||||
Signature: { is: null },
|
||||
},
|
||||
include: {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Button, IconButton } from "@documenso/ui";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const SignPage: NextPageWithLayout = (props: any) => {
|
||||
const Signed: NextPageWithLayout = (props: any) => {
|
||||
const router = useRouter();
|
||||
const allRecipientsSigned = props.document.Recipient?.every(
|
||||
(r: any) => r.signingStatus === "SIGNED"
|
||||
@@ -47,7 +47,12 @@ const SignPage: NextPageWithLayout = (props: any) => {
|
||||
onClick={(event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
router.push("/api/documents/" + props.document.id);
|
||||
router.push(
|
||||
"/api/documents/" +
|
||||
props.document.id +
|
||||
"?token=" +
|
||||
props.recipient.token
|
||||
);
|
||||
}}
|
||||
>
|
||||
Download "{props.document.title}"
|
||||
@@ -103,8 +108,9 @@ export async function getServerSideProps(context: any) {
|
||||
props: {
|
||||
document: JSON.parse(JSON.stringify(recipient.Document)),
|
||||
fields: JSON.parse(JSON.stringify(fields)),
|
||||
recipient: JSON.parse(JSON.stringify(recipient)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default SignPage;
|
||||
export default Signed;
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import Head from "next/head";
|
||||
import Login from "../components/login";
|
||||
|
||||
export default function LoginPage() {
|
||||
export default function LoginPage(props: any) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Login | Documenso</title>
|
||||
</Head>
|
||||
<Login></Login>
|
||||
<Login allowSignup={props.ALLOW_SIGNUP}></Login>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const ALLOW_SIGNUP = process.env.ALLOW_SIGNUP === "true";
|
||||
|
||||
return {
|
||||
props: {
|
||||
ALLOW_SIGNUP: ALLOW_SIGNUP,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,14 @@ export default function SignupPage(props: { source: string }) {
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
if (process.env.ALLOW_SIGNUP !== "true")
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const signupSource: string = context.query["source"];
|
||||
return {
|
||||
props: {
|
||||
|
||||
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",
|
||||
"build": "npm i && cd apps && cd web && npm i && next build",
|
||||
"start": "cd apps && cd web && next start",
|
||||
"db-migrate:dev": "prisma migrate dev",
|
||||
"db-seed": "prisma db seed",
|
||||
"db-studio": "prisma studio"
|
||||
},
|
||||
@@ -24,6 +25,7 @@
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/node": "18.11.9",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@types/react-signature-canvas": "^1.0.2",
|
||||
"avatar-from-initials": "^1.0.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dotenv": "^16.0.3",
|
||||
@@ -31,7 +33,7 @@
|
||||
"eslint-config-next": "13.0.3",
|
||||
"install": "^0.13.0",
|
||||
"next": "13.0.3",
|
||||
"next-auth": "^4.18.3",
|
||||
"next-auth": ">=4.20.1",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"npm": "^9.1.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
@@ -41,9 +43,5 @@
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-signature-canvas": "^1.0.6",
|
||||
"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]) {
|
||||
const body = new FormData();
|
||||
const document = event.target.files[0];
|
||||
const fileName = event.target.files[0].name;
|
||||
const fileName: string = event.target.files[0].name;
|
||||
|
||||
if (!fileName.endsWith(".pdf")) {
|
||||
toast.error("Non-PDF documents are not supported yet.");
|
||||
return;
|
||||
}
|
||||
body.append("document", document || "");
|
||||
const response: any = await toast
|
||||
.promise(
|
||||
|
||||
@@ -2,11 +2,12 @@ import toast from "react-hot-toast";
|
||||
|
||||
export const createOrUpdateField = async (
|
||||
document: any,
|
||||
field: any
|
||||
field: any,
|
||||
recipientToken: string = ""
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const created = await toast.promise(
|
||||
fetch("/api/documents/" + document.id + "/fields", {
|
||||
fetch("/api/documents/" + document.id + "/fields?token=" + recipientToken, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -11,14 +11,29 @@ export const sendMail = async (
|
||||
content: string | Buffer;
|
||||
}[] = []
|
||||
) => {
|
||||
if (!process.env.SENDGRID_API_KEY)
|
||||
throw new Error("Sendgrid API Key not set.");
|
||||
let transport;
|
||||
if (process.env.SENDGRID_API_KEY)
|
||||
transport = nodemailer.createTransport(
|
||||
nodemailerSendgrid({
|
||||
apiKey: process.env.SENDGRID_API_KEY || "",
|
||||
})
|
||||
);
|
||||
|
||||
if (process.env.SMTP_MAIL_HOST)
|
||||
transport = nodemailer.createTransport({
|
||||
host: process.env.SMTP_MAIL_HOST || "",
|
||||
port: Number(process.env.SMTP_MAIL_PORT) || 587,
|
||||
auth: {
|
||||
user: process.env.SMTP_MAIL_USER || "",
|
||||
pass: process.env.SMTP_MAIL_PASSWORD || "",
|
||||
},
|
||||
});
|
||||
|
||||
if (!transport)
|
||||
throw new Error(
|
||||
"No valid transport for NodeMailer found. Probably Sendgrid API Key nor SMTP Mail host was set."
|
||||
);
|
||||
|
||||
const transport = await nodemailer.createTransport(
|
||||
nodemailerSendgrid({
|
||||
apiKey: process.env.SENDGRID_API_KEY || "",
|
||||
})
|
||||
);
|
||||
await transport
|
||||
.sendMail({
|
||||
from: process.env.MAIL_FROM,
|
||||
|
||||
@@ -4,16 +4,16 @@ import { SendStatus, ReadStatus, DocumentStatus } from "@prisma/client";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||
import { signingRequestTemplate } from "@documenso/lib/mail";
|
||||
|
||||
export const sendSigningRequest = async (
|
||||
recipient: any,
|
||||
document: any,
|
||||
user: any
|
||||
) => {
|
||||
export const sendSigningRequest = async (recipient: any, document: any, user: any) => {
|
||||
const signingRequestMessage = user.name
|
||||
? `${user.name} (${user.email}) has sent you a document to sign. `
|
||||
: `${user.email} has sent you a document to sign. `;
|
||||
|
||||
await sendMail(
|
||||
recipient.email,
|
||||
`Please sign ${document.title}`,
|
||||
signingRequestTemplate(
|
||||
`${user.name} (${user.email}) has sent you a document to sign. `,
|
||||
signingRequestMessage,
|
||||
document,
|
||||
recipient,
|
||||
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${document.id}/sign?token=${recipient.token}`,
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function getUserFromToken(
|
||||
const tokenEmail = token?.email?.toString();
|
||||
|
||||
if (!token) {
|
||||
res.status(401).send("No session token found for request.");
|
||||
if (res.status) res.status(401).send("No session token found for request.");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { SelectBox } from "./components/selectBox/index";
|
||||
export { Breadcrumb } from "./components/breadcrumb/index";
|
||||
export { Dialog } from "./components/dialog/index";
|
||||
|
||||
Reference in New Issue
Block a user