Compare commits

...

74 Commits

Author SHA1 Message Date
Mythie 8457823d8e fix: improve sign in and sign up pages 2023-05-01 20:01:36 +10:00
Mythie d135df827a fix: improve general styling
Improve the general styling of the app by removing floats and replacing it `flex`. Additionally, improve the constrast of certain parts of the app and add some transitions to hover changes.
2023-05-01 20:01:35 +10:00
Timur Ercan d2301a923b Merge pull request #140 from documenso/feat/DOC-210-sign-dialog-broken-on-second-opening
fix: debounce display of signing canvas
2023-04-28 19:12:10 +02:00
Timur Ercan 108614bf46 Merge branch 'main' into feat/DOC-210-sign-dialog-broken-on-second-opening 2023-04-28 18:22:57 +02:00
Timur Ercan adf69edd54 Merge pull request #141 from documenso/fix/DOC-214-date-field-appears-for-all-recipients
fix: date field appears for all recipients
2023-04-28 18:20:52 +02:00
Timur Ercan 82139f6b2d Merge branch 'main' into fix/DOC-214-date-field-appears-for-all-recipients 2023-04-25 11:51:23 +02:00
Lucas Smith 270c82759c Merge pull request #137 from zahid47/issue-131-redirect-to-dashboard-if-logged-in
Redirect to /dashboard if auth user tries to access /login or /signup
2023-04-25 11:15:11 +10:00
Lucas Smith 01c7903efa Merge pull request #142 from raysubham/fix/keep-url-state-in-sync
feat: Keep the URL query params and UI state in sync when status filter changes
2023-04-25 10:48:30 +10:00
Lucas Smith 64b755d5ba Merge branch 'main' into fix/keep-url-state-in-sync 2023-04-25 10:48:07 +10:00
Lucas Smith 8788b64585 Merge pull request #143 from mikeriss/fix-typo
Fix: typos on Readme
2023-04-25 10:41:27 +10:00
mikeriss c9547057f6 fixed addional typos
typos fixed
2023-04-24 19:59:56 +02:00
mikeriss 17e688c222 typo
changed machnine to machine
2023-04-24 19:51:05 +02:00
mikeriss f5a42e694d Updated README.md typo
changed a typo from signging to signing
2023-04-24 19:48:34 +02:00
Subham Ray b2d09216c8 rename function 2023-04-24 23:13:38 +05:30
Subham Ray 6d30a486ab added type for statusFilter 2023-04-24 19:37:41 +05:30
Subham Ray dc6217b14e feat(Documents Filter): Keep the URL and UI state in sync when status filter changes 2023-04-24 19:16:56 +05:30
Lucas Smith a6171ec4f3 Merge branch 'main' into fix/DOC-214-date-field-appears-for-all-recipients 2023-04-23 10:36:17 +10:00
Timur Ercan d0f962598c Merge branch 'main' into feat/DOC-210-sign-dialog-broken-on-second-opening 2023-04-21 15:49:40 +02:00
Mythie 81fd9ff749 fix: date field appears for all recipients
Updates the signing endpoint to only apply changes to the Date field for the current signer. This is made possible through the addition of the `signedAt` column within the database.

Resolves the issue with one signer filling the date for all recipients and also ensures that the date of signing on a document won't always be todays date after each recipient has signed.
2023-04-21 23:43:54 +10:00
Mythie 4dcb0a684d fix: debounce display of signing canvas
Debounces the display of the signing canvas to avoid situtations where the canvas renders to 2px due to rendering while a transition is being performed.
2023-04-21 23:18:36 +10:00
Timur Ercan ab96990d43 render PR env debug 2023-04-21 13:29:51 +02:00
Timur Ercan ad5b2bcf82 fix: pr env condition 2023-04-21 12:59:53 +02:00
Timur Ercan 6f18be6b5b add render preview env support 2023-04-21 12:42:31 +02:00
Timur Ercan 8039871ab1 Merge pull request #130 from Mythie/fix/can-add-signature-space-for-empty-recipients
fix: disable selection for draft recipients
2023-04-20 17:26:01 +02:00
Timur Ercan 4b9840d7e0 Merge branch 'main' into fix/can-add-signature-space-for-empty-recipients 2023-04-20 17:25:39 +02:00
Timur Ercan 544a16caff Merge pull request #135 from Mythie/fix/signing-email-breaks-on-small-decices
fix: signing email breaks on small devices
2023-04-20 17:19:21 +02:00
Timur Ercan 989d036e54 Merge branch 'main' into fix/signing-email-breaks-on-small-decices 2023-04-20 17:14:00 +02:00
Lucas Smith 894f8720b8 Merge pull request #134 from SauravL3010/bugfix-#71/invalid-email-hint
Toast error for invalid email
2023-04-19 23:58:13 +10:00
Mythie 70ea3ceaf3 fix: improve types 2023-04-19 23:56:39 +10:00
Saurav Gurhale 80d26adf9c add toast error for invalid email 2023-04-19 23:56:39 +10:00
Lucas Smith b4e21f97e3 Merge pull request #133 from dephraiim/docker-container-name
Use `documenso` as container name for local development
2023-04-19 23:32:00 +10:00
Lucas Smith 52f554a636 Merge pull request #136 from dephraiim/doc-223
Remove Input Placeholders
2023-04-19 22:55:26 +10:00
zahid 849885b5b3 fix: redirect to /dashboard if auth user tries to access /login or /signup 2023-04-19 13:11:02 +06:00
Ephraim Atta-Duncan bcc2530484 Declutter Textarea by removing placeholders 2023-04-16 23:45:57 +00:00
Mythie d863f89232 fix: signing email breaks on small devices
Currently the signing email displays poorly on small devices with the line wrapping
causing the button to look broken.

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

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

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

You can see the improved accuracy in action here:

https://www.loom.com/share/1095fee7605c4790b8b30f573a04f0f0
2023-04-06 23:34:53 +10:00
Timur Ercan e3db462587 Merge pull request #34 from dephraiim/prettier-config
[new] Add `prettier`
2023-04-06 15:03:39 +02:00
Timur Ercan 739d29d753 Merge branch 'main' into prettier-config 2023-04-06 15:02:17 +02:00
Ephraim Atta-Duncan 964e749039 Update prettier styling 2023-04-04 22:10:30 +00:00
Ephraim Atta-Duncan 84b57d715c Apply prettier config to all files 2023-04-04 22:02:32 +00:00
Ephraim Atta-Duncan 85f2b5e84a Add prettier config 2023-04-04 22:00:01 +00:00
115 changed files with 3060 additions and 1443 deletions
+38
View File
@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
**/node_modules
**/.pnp
**.pnp.js
# testing
**/coverage
# next.js
**/.next/
**/out/
# production
**/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env
.env.example
+10 -1
View File
@@ -1,6 +1,9 @@
# Database
# Option 1: You can use the provided remote test database, courtesy of the documenso team: postgres://documenso_test_user:GnmLG14u12sd9zHsd4vVWwP40WneFJMo@dpg-cf2hljh4reb5o45oqpq0-a.oregon-postgres.render.com/documenso_test_e2i3
# Option 2: Set up a local Postgres SQL instance (RECOMMENDED)
# Option 3: Use the provided dx setup (RECOMMENDED)
# => postgres://documenso:password@127.0.0.1:54320/documenso
#
# ⚠ WARNING: The test database can be resetted or taken offline at any point.
# ⚠ WARNING: Please be aware that nothing written to the test databae is private.
DATABASE_URL=''
@@ -20,6 +23,12 @@ SENDGRID_API_KEY=''
# SMTP
# Set SMTP credentials to use SMTP instead of the Sendgrid API.
# If you're using the dx setup you can use the following values:
#
# SMTP_MAIL_HOST='127.0.0.1'
# SMTP_MAIL_PORT='2500'
# SMTP_MAIL_USER='documenso'
# SMTP_MAIL_PASSWORD='documenso'
SMTP_MAIL_HOST=''
SMTP_MAIL_PORT=''
SMTP_MAIL_USER=''
@@ -30,4 +39,4 @@ MAIL_FROM='documenso@localhost.com'
#FEATURE FLAGS
# Allow users to register via the /signup page. Otherwise they will be redirect to the home page.
ALLOW_SIGNUP=true
ALLOW_SIGNUP=true
-3
View File
@@ -1,3 +0,0 @@
[submodule "apps/website/documenso/website"]
path = apps/website/documenso/website
url = http://github.com/documenso/website.git
+47 -10
View File
@@ -78,7 +78,7 @@ The current project goal is to <b>[release a production ready version](https://g
Documenso is built using awesome open source tech including:
- [Typescript](https://www.typescriptlang.org/)
- [Javascript (when neccessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [Javascript (when necessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [NextJS (JS Fullstack Framework)](https://nextjs.org/)
- [Postgres SQL (Database)](https://www.postgresql.org/)
- [Prisma (ORM - Object-relational mapping)](https://www.prisma.io/)
@@ -96,12 +96,39 @@ Documenso is built using awesome open source tech including:
To run Documenso locally you need
- [Node.js (Version: >=18.x)](https://nodejs.org/en/download/)
- Node Package Manger NPM - included in Node.js
- Node Package Manager NPM - included in Node.js
- [PostgreSQL (local or remote)](https://www.postgresql.org/download/)
## Developer Quickstart
> **Note**: This is a quickstart for developers. It assumes that you have both [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/) installed on your machine.
Want to get up and running quickly? Follow these steps:
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh
git clone https://github.com/documenso/documenso
```
- Set up your .env file using the recommendations in the .env.example file.
- Run `npm run dx` in the root directory
- This will spin up a postgres database and inbucket mail server in docker containers.
- Run `npm run dev` in the root directory
- Want it even faster? Just use
```sh
npm run d
```
That's it! You should now be able to access the app at http://localhost:3000
Incoming mail will be available at http://localhost:9000
Your database will also be available on port `5432`. You can connect to it using your favorite database client.
## Developer Setup
Follow these steps to setup documenso on you local machnine:
Follow these steps to setup documenso on you local machine:
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh
@@ -111,35 +138,36 @@ Follow these steps to setup documenso on you local machnine:
- 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)
- Or setup a local postgres sql instance (recommended)
- 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
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the <code>SMTP\_\* variables</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**.
- To generate your own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signing certificate**.
## 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 necessary to regenerate your database client
- You can do this by running the generate command in /packages/prisma:
```sh
npx prisma generate
```
- This is not neccessary on first clone
- This is not necessary on first clone
# Creating your own signging certificate
# Creating your own signing certificate
For the digital signature of you documents you need a signign certificate in .p12 formate (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
For the digital signature of your documents you need a signing certificate in .p12 formate (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:\
<code>openssl genrsa -out private.key 2048</code>
@@ -152,6 +180,15 @@ For the digital signature of you documents you need a signign certificate in .p1
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
5. Place the certificate <code>/apps/web/ressource/certificate.p12</code>
# Docker
> We are still working on the publishing of docker images, in the meantime you can follow the steps below to create a production ready docker image.
Want to create a production ready docker image? Follow these steps:
- Run `./docker/build.sh` in the root directory.
- Publish the image to your docker registry of choice.
# Deploying - Coming Soon™
- Docker support
+9 -11
View File
@@ -1,8 +1,9 @@
import React, { useState } from "react";
import Draggable from "react-draggable";
import Logo from "../logo";
import { IconButton } from "@documenso/ui";
import Logo from "../logo";
import { XCircleIcon } from "@heroicons/react/20/solid";
import Draggable from "react-draggable";
const stc = require("string-to-color");
type FieldPropsType = {
@@ -51,21 +52,19 @@ export default function EditableField(props: FieldPropsType) {
onMouseDown={(e: any) => {
e.preventDefault();
e.stopPropagation();
}}
>
}}>
{/* width: 192 height 96 */}
<div
hidden={props.hidden}
ref={nodeRef}
className="cursor-move opacity-80 p-2 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none"
className="absolute top-0 left-0 m-auto h-16 w-48 cursor-move select-none flex-row-reverse p-2 text-center text-lg font-bold opacity-80"
style={{
background: stc(props.field.Recipient.email),
}}
>
<div className="m-auto overflow-hidden flex-row-reverse text-lg font-bold text-center">
}}>
<div className="m-auto flex-row-reverse overflow-hidden text-center text-lg font-bold">
{field.type}
{field.type === "SIGNATURE" ? (
<div className="text-xs text-center">
<div className="text-center text-xs">
{`${props.field.Recipient?.name} <${props.field.Recipient?.email}>`}
</div>
) : (
@@ -79,8 +78,7 @@ export default function EditableField(props: FieldPropsType) {
icon={XCircleIcon}
onClick={(event: any) => {
props.onDelete(props.field.id);
}}
></IconButton>
}}></IconButton>
</strong>
</div>
</Draggable>
@@ -1,7 +1,8 @@
import { useEffect, useState } from "react";
import { RadioGroup } from "@headlessui/react";
import { classNames } from "@documenso/lib";
import { RadioGroup } from "@headlessui/react";
import { FieldType } from "@prisma/client";
const stc = require("string-to-color");
export default function FieldTypeSelector(props: any) {
@@ -24,8 +25,7 @@ export default function FieldTypeSelector(props: any) {
value={selectedFieldType}
onChange={(e: any) => {
setSelectedFieldType(e);
}}
>
}}>
<div className="space-y-4">
{fieldTypes.map((fieldType) => (
<RadioGroup.Option
@@ -37,30 +37,23 @@ export default function FieldTypeSelector(props: any) {
className={({ checked, active }) =>
classNames(
checked ? "border-neon border-2" : "border-transparent",
"hover:bg-slate-100 select-none relative block cursor-pointer rounded-lg border bg-white px-3 py-2 focus:outline-none sm:flex sm:justify-between"
"relative block cursor-pointer select-none rounded-lg border bg-white px-3 py-2 hover:bg-slate-100 focus:outline-none sm:flex sm:justify-between"
)
}
>
}>
{({ active, checked }) => (
<>
<span className="flex items-center">
<span className="flex flex-col text-sm">
<RadioGroup.Label
as="span"
className="font-medium text-gray-900"
>
<RadioGroup.Label as="span" className="font-medium text-gray-900">
<span
className="inline-block h-4 w-4 flex-shrink-0 rounded-full mr-3 align-middle"
className="mr-3 inline-block h-4 w-4 flex-shrink-0 rounded-full align-middle"
style={{
background: stc(props.selectedRecipient?.email),
}}
/>
<span className="align-middle">
{" "}
{
fieldTypes.filter((e) => e.id === fieldType.id)[0]
.name
}
{fieldTypes.filter((e) => e.id === fieldType.id)[0].name}
</span>
</RadioGroup.Label>
</span>
+17 -34
View File
@@ -1,13 +1,14 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
import { useState } from "react";
import { createOrUpdateField, deleteField } from "@documenso/lib/api";
import { createField } from "@documenso/features/editor";
import RecipientSelector from "./recipient-selector";
import FieldTypeSelector from "./field-type-selector";
import { InformationCircleIcon } from "@heroicons/react/24/outline";
import dynamic from "next/dynamic";
import Link from "next/link";
import { useRouter } from "next/router";
import { createField } from "@documenso/features/editor";
import { createOrUpdateField, deleteField } from "@documenso/lib/api";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import FieldTypeSelector from "./field-type-selector";
import RecipientSelector from "./recipient-selector";
import { InformationCircleIcon } from "@heroicons/react/24/outline";
const stc = require("string-to-color");
const PDFViewer = dynamic(() => import("./pdf-viewer"), {
@@ -20,8 +21,7 @@ export default function PDFEditor(props: any) {
const [selectedRecipient, setSelectedRecipient]: any = useState();
const [selectedFieldType, setSelectedFieldType] = useState();
const noRecipients =
props?.document.Recipient.length === 0 ||
props?.document.Recipient.every((e: any) => !e.email);
props?.document.Recipient.length === 0 || props?.document.Recipient.every((e: any) => !e.email);
function onPositionChangedHandler(position: any, id: any) {
if (!position) return;
@@ -53,26 +53,16 @@ export default function PDFEditor(props: any) {
<div hidden={!noRecipients} className="rounded-md bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-yellow-400"
aria-hidden="true"
/>
<InformationCircleIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-yellow-700">
This document does not have any recipients. Add recipients to
create fields.
This document does not have any recipients. Add recipients to create fields.
</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
<Link
href={
NEXT_PUBLIC_WEBAPP_URL +
"/documents/" +
props.document.id +
"/recipients"
}
className="whitespace-nowrap font-medium text-yellow-700 hover:text-yellow-600"
>
href={NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients"}
className="whitespace-nowrap font-medium text-yellow-700 hover:text-yellow-600">
Add Recipients
<span aria-hidden="true"> &rarr;</span>
</Link>
@@ -98,12 +88,10 @@ export default function PDFEditor(props: any) {
}}
onMouseDown={(e: any, page: number) => {
if (e.button === 0) addField(e, page);
}}
></PDFViewer>
}}></PDFViewer>
<div
hidden={noRecipients}
className="fixed left-0 top-1/3 max-w-xs border border-slate-300 bg-white py-4 pr-5 rounded-md"
>
className="fixed left-0 top-1/3 max-w-xs rounded-md border border-slate-300 bg-white py-4 pr-5">
<RecipientSelector
recipients={props?.document?.Recipient}
onChange={setSelectedRecipient}
@@ -123,12 +111,7 @@ export default function PDFEditor(props: any) {
if (!selectedFieldType) return;
if (noRecipients) return;
const signatureField = createField(
e,
page,
selectedRecipient,
selectedFieldType
);
const signatureField = createField(e, page, selectedRecipient, selectedFieldType);
createOrUpdateField(props?.document, signatureField).then((res) => {
setFields((prevState) => [...prevState, res]);
+27 -55
View File
@@ -1,21 +1,14 @@
import Logo from "../logo";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
import SignatureDialog from "./signature-dialog";
import { useEffect, useState } from "react";
import { Button } from "@documenso/ui";
import {
CheckBadgeIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
import { FieldType } from "@prisma/client";
import {
createOrUpdateField,
deleteField,
signDocument,
} from "@documenso/lib/api";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { createField } from "@documenso/features/editor";
import { createOrUpdateField, deleteField, signDocument } from "@documenso/lib/api";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { Button } from "@documenso/ui";
import Logo from "../logo";
import SignatureDialog from "./signature-dialog";
import { CheckBadgeIcon, InformationCircleIcon } from "@heroicons/react/24/outline";
import { FieldType } from "@prisma/client";
const PDFViewer = dynamic(() => import("./pdf-viewer"), {
ssr: false,
@@ -27,9 +20,7 @@ export default function PDFSigner(props: any) {
const [signingDone, setSigningDone] = useState(false);
const [localSignatures, setLocalSignatures] = useState<any[]>([]);
const [fields, setFields] = useState<any[]>(props.fields);
const signatureFields = fields.filter(
(field) => field.type === FieldType.SIGNATURE
);
const signatureFields = fields.filter((field) => field.type === FieldType.SIGNATURE);
const [dialogField, setDialogField] = useState<any>();
useEffect(() => {
@@ -80,33 +71,29 @@ export default function PDFSigner(props: any) {
<div className="bg-neon p-4">
<div className="flex">
<div className="flex-shrink-0">
<Logo className="h-12 w-12 -mt-2.5"></Logo>
<Logo className="-mt-2.5 h-12 w-12"></Logo>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between text-center justify-start items-center">
<div className="ml-3 flex-1 items-center justify-start text-center md:flex md:justify-between">
<p className="text-lg text-slate-700">
{props.document.User.name
? `${props.document.User.name} (${props.document.User.email})`
: props.document.User.email}{" "}
would like you to sign this document.
</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
<p className="mt-3 text-sm md:mt-0 md:ml-6 text-right md:text-inherit">
<Button
disabled={!signingDone}
color="secondary"
icon={CheckBadgeIcon}
className="float-right"
onClick={() => {
signDocument(
props.document,
localSignatures,
`${router.query.token}`
).then(() => {
router.push(
`/documents/${props.document.id}/signed?token=${router.query.token}`
);
});
}}
>
signDocument(props.document, localSignatures, `${router.query.token}`).then(
() => {
router.push(
`/documents/${props.document.id}/signed?token=${router.query.token}`
);
}
);
}}>
Done
</Button>
</p>
@@ -117,15 +104,11 @@ export default function PDFSigner(props: any) {
<div className="bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-yellow-400"
aria-hidden="true"
/>
<InformationCircleIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-yellow-700">
You can sign this document anywhere you like, but maybe look for
a signature line.
You can sign this document anywhere you like, but maybe look for a signature line.
</p>
</div>
</div>
@@ -144,12 +127,10 @@ export default function PDFSigner(props: any) {
pdfUrl={`${NEXT_PUBLIC_WEBAPP_URL}/api/documents/${router.query.id}?token=${router.query.token}`}
onClick={onClick}
onMouseDown={function onMouseDown(e: any, page: number) {
if (signatureFields.length === 0)
addFreeSignature(e, page, props.recipient);
if (signatureFields.length === 0) addFreeSignature(e, page, props.recipient);
}}
onMouseUp={() => {}}
onDelete={onDeleteHandler}
></PDFViewer>
onDelete={onDeleteHandler}></PDFViewer>
</>
);
@@ -166,18 +147,9 @@ export default function PDFSigner(props: any) {
}
function addFreeSignature(e: any, page: number, recipient: any): any {
const freeSignatureField = createField(
e,
page,
recipient,
FieldType.FREE_SIGNATURE
);
const freeSignatureField = createField(e, page, recipient, FieldType.FREE_SIGNATURE);
createOrUpdateField(
props.document,
freeSignatureField,
recipient.token
).then((res) => {
createOrUpdateField(props.document, freeSignatureField, recipient.token).then((res) => {
setFields((prevState) => [...prevState, res]);
setDialogField(res);
setOpen(true);
+9 -15
View File
@@ -1,9 +1,9 @@
import { Fragment, useState } from "react";
import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
import EditableField from "./editable-field";
import SignableField from "./signable-field";
import short from "short-uuid";
import { FieldType } from "@prisma/client";
import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
import short from "short-uuid";
export default function PDFViewer(props) {
const [numPages, setNumPages] = useState(null);
@@ -33,16 +33,14 @@ export default function PDFViewer(props) {
<div
hidden={loading}
onMouseUp={props.onMouseUp}
style={{ height: numPages * pageHeight + 1000 }}
>
<div className="max-w-xs mt-6"></div>
style={{ height: numPages * pageHeight + 1000 }}>
<div className="mt-6 max-w-xs"></div>
<Document
file={props.pdfUrl}
onLoadSuccess={onDocumentLoadSuccess}
options={options}
renderMode="canvas"
className="absolute w-auto mx-auto left-0 right-0"
>
className="absolute left-0 right-0 mx-auto w-auto">
{Array.from({ length: numPages }, (_, index) => (
<Fragment key={short.generate().toString()}>
<div
@@ -57,8 +55,7 @@ export default function PDFViewer(props) {
position: "relative",
...props.style,
}}
className="mx-auto w-fit"
>
className="mx-auto w-fit">
<Page
className="mt-5"
key={`page_${index + 1}`}
@@ -69,8 +66,7 @@ export default function PDFViewer(props) {
if (e.height) setPageHeight(e.height);
setLoading(false);
}}
onRenderError={() => setLoading(false)}
></Page>
onRenderError={() => setLoading(false)}></Page>
{props?.fields
.filter((field) => field.page === index)
.map((field) =>
@@ -80,8 +76,7 @@ export default function PDFViewer(props) {
key={field.id}
field={field}
className="absolute"
onDelete={onDeleteHandler}
></SignableField>
onDelete={onDeleteHandler}></SignableField>
) : (
<EditableField
hidden={
@@ -93,8 +88,7 @@ export default function PDFViewer(props) {
field={field}
className="absolute"
onPositionChanged={onPositionChangedHandler}
onDelete={onDeleteHandler}
></EditableField>
onDelete={onDeleteHandler}></EditableField>
)
)}
</div>
@@ -1,13 +1,12 @@
import { Fragment, useEffect, useState } from "react";
import { classNames } from "@documenso/lib";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline";
import { classNames } from "@documenso/lib";
const stc = require("string-to-color");
export default function RecipientSelector(props: any) {
const [selectedRecipient, setSelectedRecipient]: any = useState(
props?.recipients[0]
);
const [selectedRecipient, setSelectedRecipient]: any = useState(props?.recipients[0]);
useEffect(() => {
props.onChange(selectedRecipient);
@@ -18,11 +17,10 @@ export default function RecipientSelector(props: any) {
value={selectedRecipient}
onChange={(e: any) => {
setSelectedRecipient(e);
}}
>
}}>
{({ open }) => (
<div className="relative mt-1 mb-2">
<Listbox.Button className="select-none relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left focus:border-neon focus:outline-none focus:ring-1 focus:ring-neon sm:text-sm">
<Listbox.Button className="focus:border-neon focus:ring-neon relative w-full cursor-default select-none rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-1 sm:text-sm">
<span className="flex items-center">
<span
className="inline-block h-4 w-4 flex-shrink-0 rounded-full"
@@ -33,10 +31,7 @@ export default function RecipientSelector(props: any) {
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>
@@ -45,20 +40,19 @@ export default function RecipientSelector(props: any) {
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
leaveTo="opacity-0">
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{props?.recipients.map((recipient: any) => (
<Listbox.Option
key={recipient?.id}
disabled={!recipient?.email}
className={({ active }) =>
classNames(
active ? "text-white bg-neon-dark" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9"
active ? "bg-neon-dark text-white" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9 aria-disabled:opacity-50 aria-disabled:cursor-not-allowed"
)
}
value={recipient}
>
value={recipient}>
{({ selected, active }) => (
<>
<div className="flex items-center">
@@ -72,9 +66,8 @@ export default function RecipientSelector(props: any) {
className={classNames(
selected ? "font-semibold" : "font-normal",
"ml-3 block truncate"
)}
>
{`${selectedRecipient?.name} <${selectedRecipient?.email}>`}
)}>
{`${recipient?.name} <${recipient?.email || 'unknown'}>`}
</span>
</div>
@@ -83,9 +76,8 @@ export default function RecipientSelector(props: any) {
className={classNames(
active ? "text-white" : "text-neon-dark",
"absolute inset-y-0 right-0 flex items-center pr-4"
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
)}>
<CheckIcon className="h-5 w-5" strokeWidth={3} aria-hidden="true" />
</span>
) : null}
</>
+10 -14
View File
@@ -1,8 +1,9 @@
import React, { useState } from "react";
import Draggable from "react-draggable";
import { classNames } from "@documenso/lib";
import { IconButton } from "@documenso/ui";
import { XCircleIcon } from "@heroicons/react/20/solid";
import { classNames } from "@documenso/lib";
import Draggable from "react-draggable";
const stc = require("string-to-color");
type FieldPropsType = {
@@ -37,31 +38,26 @@ export default function SignableField(props: FieldPropsType) {
onMouseDown={(e: any) => {
// e.preventDefault();
e.stopPropagation();
}}
>
}}>
<div
onClick={(e: any) => {
if (!field?.signature) props.onClick(props.field);
}}
ref={nodeRef}
className={classNames(
"opacity-80 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none",
field.type === "SIGNATURE"
? "cursor-pointer hover:brightness-50"
: "cursor-not-allowed"
"absolute top-0 left-0 m-auto h-16 w-48 select-none flex-row-reverse text-center text-lg font-bold opacity-80",
field.type === "SIGNATURE" ? "cursor-pointer hover:brightness-50" : "cursor-not-allowed"
)}
style={{
background: stc(props.field.Recipient.email),
}}
>
<div hidden={field?.signature} className="font-medium my-4">
}}>
<div hidden={field?.signature} className="my-4 font-medium">
{field.type === "SIGNATURE" ? "SIGN HERE" : ""}
{field.type === "DATE" ? <small>Date (filled on sign)</small> : ""}
</div>
<div
hidden={!field?.signature}
className="font-qwigley text-5xl m-auto w-auto flex-row-reverse font-medium text-center"
>
className="font-qwigley m-auto w-auto flex-row-reverse text-center text-5xl font-medium">
{field?.signature?.type === "type" ? (
<div className="my-4">{field?.signature.typedSignature}</div>
) : (
@@ -69,7 +65,7 @@ export default function SignableField(props: FieldPropsType) {
)}
{field?.signature?.type === "draw" ? (
<img className="w-48 h-16" src={field?.signature?.signatureImage} />
<img className="h-16 w-48" src={field?.signature?.signatureImage} />
) : (
""
)}
+70 -76
View File
@@ -1,14 +1,11 @@
import { Fragment, useEffect, useState } from "react";
import { classNames } from "@documenso/lib";
import { localStorage } from "@documenso/lib";
import { Button, IconButton } from "@documenso/ui";
import { Dialog, Transition } from "@headlessui/react";
import {
LanguageIcon,
PencilIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { Fragment, useEffect, useState } from "react";
import { LanguageIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
import SignatureCanvas from "react-signature-canvas";
import { localStorage } from "@documenso/lib";
import { useDebouncedValue } from "../../hooks/use-debounced-value";
const tabs = [
{ name: "Type", icon: LanguageIcon, current: true },
@@ -19,6 +16,9 @@ export default function SignatureDialog(props: any) {
const [currentTab, setCurrentTab] = useState(tabs[0]);
const [typedSignature, setTypedSignature] = useState("");
const [signatureEmpty, setSignatureEmpty] = useState(true);
// This is a workaround to prevent the canvas from being rendered when the dialog is closed
// we also need the debounce to avoid rendering while transitions are occuring.
const showCanvas = useDebouncedValue<boolean>(props.open, 1);
let signCanvasRef: any | undefined;
useEffect(() => {
@@ -34,8 +34,7 @@ export default function SignatureDialog(props: any) {
onClose={() => {
props.setOpen(false);
setCurrent(tabs[0]);
}}
>
}}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@@ -43,8 +42,7 @@ export default function SignatureDialog(props: any) {
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
leaveTo="opacity-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
@@ -57,11 +55,10 @@ export default function SignatureDialog(props: any) {
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="min-h-[350px] relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<Dialog.Panel className="relative min-h-[350px] transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
<div className="">
<div className="border-b border-gray-200 mb-3">
<div className="mb-3 border-b border-gray-200">
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<a
@@ -72,11 +69,10 @@ export default function SignatureDialog(props: any) {
className={classNames(
tab.current
? "border-neon text-neon"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
"group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm cursor-pointer"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"group inline-flex cursor-pointer items-center border-b-2 py-4 px-1 text-sm font-medium"
)}
aria-current={tab.current ? "page" : undefined}
>
aria-current={tab.current ? "page" : undefined}>
<tab.icon
className={classNames(
tab.current
@@ -93,7 +89,7 @@ export default function SignatureDialog(props: any) {
</div>
{isCurrentTab("Type") ? (
<div>
<div className="my-8 border-b border-gray-300 mb-3">
<div className="my-7 mb-3 border-b border-gray-300">
<input
value={typedSignature}
onChange={(e) => {
@@ -101,36 +97,31 @@ export default function SignatureDialog(props: any) {
}}
className={classNames(
typedSignature ? "font-qwigley text-4xl" : "",
"leading-none h-10 align-bottom mt-14 text-center block w-full focus:border-neon focus:ring-neon text-2xl"
"focus:border-neon focus:ring-neon mt-14 block h-10 w-full text-center align-bottom text-2xl leading-none"
)}
placeholder="Kindly type your name"
/>
</div>
<div className="float-right">
<div className="flex flex-row-reverse items-center gap-x-3">
<Button
color="secondary"
onClick={() => {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
}}
>
}}>
Cancel
</Button>
<Button
className="ml-3"
disabled={!typedSignature}
onClick={() => {
localStorage.setItem(
"typedSignature",
typedSignature
);
localStorage.setItem("typedSignature", typedSignature);
props.onClose({
type: "type",
typedSignature: typedSignature,
});
}}
>
}}>
Sign
</Button>
</div>
@@ -139,52 +130,55 @@ export default function SignatureDialog(props: any) {
""
)}
{isCurrentTab("Draw") ? (
<div className="">
<SignatureCanvas
ref={(ref) => {
signCanvasRef = ref;
}}
canvasProps={{
className:
"sigCanvas border-b b-2 border-slate w-full h-full mb-3",
}}
clearOnResize={true}
onEnd={() => {
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
/>
<IconButton
className="block float-left"
icon={TrashIcon}
onClick={() => {
signCanvasRef?.clear();
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
></IconButton>
<div className="mt-10 float-right">
<Button
color="secondary"
onClick={() => {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
<div className="" key={props.open ? "closed" : "open"}>
{showCanvas && (
<SignatureCanvas
ref={(ref) => {
signCanvasRef = ref;
}}
>
Cancel
</Button>
<Button
className="ml-3"
onClick={() => {
props.onClose({
type: "draw",
signatureImage:
signCanvasRef.toDataURL("image/png"),
});
canvasProps={{
className: "sigCanvas border-b b-2 border-slate w-full h-full mb-3",
}}
disabled={signatureEmpty}
>
Sign
</Button>
clearOnResize={true}
onEnd={() => {
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
/>
)}
<div className="flex items-center justify-between">
<IconButton
className="block"
icon={TrashIcon}
onClick={() => {
signCanvasRef?.clear();
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
/>
<div className="flex flex-row-reverse items-center gap-x-3">
<Button
color="secondary"
onClick={() => {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
}}>
Cancel
</Button>
<Button
className="ml-3"
onClick={() => {
props.onClose({
type: "draw",
signatureImage: signCanvasRef.toDataURL("image/png"),
});
}}
disabled={signatureEmpty}>
Sign
</Button>
</div>
</div>
</div>
) : (
@@ -200,11 +194,11 @@ export default function SignatureDialog(props: any) {
</>
);
function isCurrentTab(tabName: string): boolean {
function isCurrentTab(tabName: string): boolean {
return currentTab.name === tabName;
}
function setCurrent(t: any) {
function setCurrent(t: any) {
tabs.forEach((tab) => {
tab.current = tab.name === t.name;
});
+1 -2
View File
@@ -1,9 +1,8 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import Navigation from "./navigation";
import { useSession } from "next-auth/react";
function useRedirectToLoginIfUnauthenticated() {
const { data: session, status } = useSession();
+16 -31
View File
@@ -1,14 +1,13 @@
import { LockClosedIcon } from "@heroicons/react/20/solid";
import Link from "next/link";
import { FormProvider, useForm } from "react-hook-form";
import Logo from "./logo";
import { signIn } from "next-auth/react";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { toast } from "react-hot-toast";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { Button } from "@documenso/ui";
import Logo from "./logo";
import { LockClosedIcon } from "@heroicons/react/20/solid";
import { signIn } from "next-auth/react";
import { FormProvider, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
interface LoginValues {
email: string;
@@ -22,10 +21,7 @@ export default function Login(props: any) {
const methods = useForm<LoginValues>();
const { register, formState } = methods;
const [errorMessage, setErrorMessage] = useState<string | null>(null);
let callbackUrl =
typeof router.query?.callbackUrl === "string"
? router.query.callbackUrl
: "";
let callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : "";
// If not absolute URL, make it absolute
if (!/^https?:\/\//.test(callbackUrl)) {
@@ -79,10 +75,7 @@ export default function Login(props: any) {
</h2>
</div>
<FormProvider {...methods}>
<form
className="mt-8 space-y-6"
onSubmit={methods.handleSubmit(onSubmit)}
>
<form className="mt-8 space-y-6" onSubmit={methods.handleSubmit(onSubmit)}>
<input type="hidden" name="remember" defaultValue="true" />
<div className="-space-y-px rounded-md shadow-sm">
<div>
@@ -96,7 +89,7 @@ export default function Login(props: any) {
type="email"
autoComplete="email"
required
className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="Email"
/>
</div>
@@ -111,14 +104,14 @@ export default function Login(props: any) {
type="password"
autoComplete="current-password"
required
className="relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="Password"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="text-sm">
<a href="#" className="font-medium text-neon hover:text-neon">
<a href="#" className="text-gray-500 hover:text-neon-700 font-medium">
Forgot your password?
</a>
</div>
@@ -127,11 +120,10 @@ export default function Login(props: any) {
<Button
type="submit"
disabled={formState.isSubmitting}
className="group relative flex w-full"
>
className="group relative flex w-full">
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
<LockClosedIcon
className="h-5 w-5 text-neon-dark group-hover:text-neon disabled:group-hover:bg-gray-600 disabled:disabled:bg-gray-600"
className="text-neon-700 group-hover:text-neon-dark-700 h-5 w-5 disabled:disabled:bg-gray-600 disabled:group-hover:bg-gray-600 duration-200"
aria-hidden="true"
/>
</span>
@@ -140,10 +132,7 @@ export default function Login(props: any) {
</div>
<div>
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center"></div>
@@ -152,10 +141,7 @@ export default function Login(props: any) {
{props.allowSignup ? (
<p className="mt-2 text-center text-sm text-gray-600">
Are you new here?{" "}
<Link
href="/signup"
className="font-medium text-neon hover:text-neon"
>
<Link href="/signup" className="text-gray-500 hover:text-neon-700 duration-200 font-medium">
Create a new Account
</Link>
</p>
@@ -164,8 +150,7 @@ export default function Login(props: any) {
Like Documenso{" "}
<Link
href="https://documenso.com"
className="font-medium text-neon hover:text-neon"
>
className="text-neon hover:text-neon font-medium">
Hosted Documenso will be availible soon
</Link>
</p>
+4 -13
View File
@@ -1,25 +1,16 @@
import { classNames } from "@documenso/lib";
import Link from "next/link";
import { classNames } from "@documenso/lib";
export default function Logo(props: any) {
return (
<>
<Link href="/dashboard">
<svg
className="w-12"
viewBox="0 0 88.6758041381836 32.18000030517578"
{...props}
>
<rect
width="88.6758041381836"
height="32.18000030517578"
fill="transparent"
></rect>
<svg className="w-12" viewBox="0 0 88.6758041381836 32.18000030517578" {...props}>
<rect width="88.6758041381836" height="32.18000030517578" fill="transparent"></rect>
<g transform="matrix(1,0,0,1,-25.98720359802246,-66.41200256347656)">
<path
d="M99.004,68.26l-.845-1.848-23.226,2.299c-1.437,.142-2.876,.089-4.3-.156-2.949-.51-6.32-1.717-9.073-2.099-1.477-.206-2.934,.448-3.789,1.67-1.174,1.678-2.308,3.888-3.501,5.622-1.518,2.207-3.032,4.418-4.531,6.638-1.693,2.499-3.365,5.013-5.061,7.51-.103,.153-.333,.221-.503,.329-.079-.279-.306-.636-.206-.824,1.006-1.928,1.845-4,3.165-5.694,1.908-2.449,2.914-5.445,5.007-7.745,.342-.777,.845-1.466,1.151-2.244,.915-2.341-1.295-5.305-2.935-5.305h-.613c-1.196,0-2.526,.357-4.092,1.716-.986,.856-2.391,2.432-2.97,3.326-3.424,5.286-7.177,10.382-10.15,15.932-.365,.682-1.719,3.722-2.013,3.606-.214-.077-.159-.458-.041-.823,1.312-4.065,4.163-9.851,5.843-13.777,.41-.962,.635-1.516-.305-2.361-1.016-.913-2.669,.084-3.084,1.052-1.784,4.174-2.631,8.08-4.038,12.396-.466,1.43-.916,2.865-1.386,4.294-.273,.831-.548,1.661-.836,2.488-.112,.321-.226,.642-.345,.962-.032,.082-.064,.165-.095,.248-.006,.011-.003,.002-.009,.017-.005,.008-.003,.015-.006,.023-.011,.026-.021,.05-.03,.076-.024,.067-.012,.044,.009-.002-.691,1.577,.417,3.002,2.091,3.002,.808,0,1.552-1.431,1.943-2.055,1.224-1.957,3.28-5.125,4.36-7.163,.894-1.69,2.078-3.14,3.209-4.615,1.857-2.42,3.065-5.242,5.025-7.575,.33-.394,.694-.772,1.093-1.093,.121-.098,.473-.05,.618,.062,.124,.098,.2,.432,.127,.577-.397,.788-.863,1.542-1.278,2.322-1.481,2.79-2.953,5.582-4.425,8.377-1.16,2.204-2.659,5.055-3.551,6.755-.438,.833-.176,1.857,.604,2.382,0,0-.383,2.028,1.908,2.028,1.896,0,2.711-1.145,3.053-1.624,.348-.486,.748-.951,1.202-1.334,.151-.13,.563-.018,.821,.079,.076,.029,.085,.406,.03,.582-.398,1.272-.323,2.283,1.879,2.283,.784,0,2.218-1.283,2.904-2.213,.794-1.075,.731-1.1,1.415-2.244,1.678-2.821,3.132-5.055,4.751-7.91,1.083-1.91,2.175-3.854,3.294-5.516,.685-1.015,1.446-1.252,2.188-1.252s.409,.686,.336,1.025c-.071,.341-.677,1.545-2.531,4.35-1.115,1.687-2.244,3.367-3.308,5.084-1.075,1.736-2.141,3.48-3.205,5.225-.88,1.445,.162,3.467,1.854,3.467h2.765c2.653,0,5.302-.397,7.916-.851l7.41-1.287c1.421-.245,2.868-.298,4.303-.158l23.161,2.296,.845-1.849c4.61-9.858,15.211-11.322,15.66-11.38v-5.717c-.448-.058-11.05-1.522-15.66-11.381Zm-40.143,6.165c-.164,.465-.491,.922-.86,1.255-.918,.828-1.451,1.842-1.911,2.977-.512,1.269-1.829,2.119-1.966,3.65-.027,.3-.627,.577-1.006,.797-.109,.062-.357-.114-.524-.174,.015-.229-.024-.401,.039-.515,.848-1.495,1.678-3.002,2.584-4.462,.775-1.254,1.642-2.453,2.469-3.677,.085-.126,.158-.273,.27-.365,.457-.368,.93-.719,1.399-1.075,.497,.721-.312,1.071-.494,1.589Zm30.047,12.011c1.736,0,3.162-1.137,3.686-2.693h10.547c-3.02,1.916-6.1,4.684-8.402,8.716l-21.902-2.17v-15.584l21.902-2.167c2.302,4.032,5.382,6.802,8.402,8.714h-10.547c-.524-1.554-1.951-2.691-3.686-2.691-2.172,0-3.938,1.763-3.938,3.938s1.766,3.938,3.938,3.938Z"
className={classNames(props.dark ? "fill-white" : "fill-brown")}
></path>
className={classNames(props.dark ? "fill-white" : "fill-brown")}></path>
</g>
</svg>
</Link>
+25 -35
View File
@@ -1,23 +1,22 @@
import { Fragment, useEffect, useState } from "react";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { signOut, useSession } from "next-auth/react";
import avatarFromInitials from "avatar-from-initials";
import { toast } from "react-hot-toast";
import { getUser } from "@documenso/lib/api";
import Logo from "./logo";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import {
ArrowRightOnRectangleIcon,
Bars3Icon,
BellIcon,
XMarkIcon,
UserCircleIcon,
ArrowRightOnRectangleIcon,
DocumentTextIcon,
ChartBarIcon,
DocumentTextIcon,
UserCircleIcon,
WrenchIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import Logo from "./logo";
import { getUser } from "@documenso/lib/api";
import avatarFromInitials from "avatar-from-initials";
import { signOut, useSession } from "next-auth/react";
import { toast } from "react-hot-toast";
const navigation = [
{
@@ -125,14 +124,12 @@ export default function TopNavigation() {
item.current
? "border-neon text-brown"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
"inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium"
)}
aria-current={item.current ? "page" : undefined}
>
aria-current={item.current ? "page" : undefined}>
<item.icon
className="flex-shrink-0 -ml-1 mr-3 h-6 w-6 inline"
aria-hidden="true"
></item.icon>
className="-ml-1 mr-3 inline h-6 w-6 flex-shrink-0"
aria-hidden="true"></item.icon>
{item.name}
</Link>
))}
@@ -142,8 +139,7 @@ export default function TopNavigation() {
onClick={() => {
document?.getElementById("mb")?.click();
}}
className="hidden sm:ml-6 sm:flex sm:items-center hover:bg-gray-200 px-3 cursor-pointer"
>
className="hidden cursor-pointer px-3 hover:bg-gray-200 sm:ml-6 sm:flex sm:items-center">
<span className="text-sm">
<p className="font-bold">{user?.name || ""}</p>
<p>{user?.email}</p>
@@ -152,8 +148,7 @@ export default function TopNavigation() {
<div>
<Menu.Button
id="mb"
className="flex max-w-xs items-center rounded-full bg-white text-sm"
>
className="flex max-w-xs items-center rounded-full bg-white text-sm">
<span className="sr-only">Open user menu</span>
<div
key={user?.email}
@@ -170,8 +165,7 @@ export default function TopNavigation() {
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
leaveTo="transform opacity-0 scale-95">
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
@@ -182,12 +176,10 @@ export default function TopNavigation() {
className={classNames(
active ? "bg-gray-100" : "",
"block px-4 py-2 text-sm text-gray-700"
)}
>
)}>
<item.icon
className="flex-shrink-0 -ml-1 mr-3 h-6 w-6 inline"
aria-hidden="true"
></item.icon>
className="-ml-1 mr-3 inline h-6 w-6 flex-shrink-0"
aria-hidden="true"></item.icon>
{item.name}
</Link>
)}
@@ -219,15 +211,14 @@ export default function TopNavigation() {
href={item.href}
className={classNames(
item.current
? "bg-teal-50 border-teal-500 text-teal-700"
: "border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800",
"block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
? "border-teal-500 bg-teal-50 text-teal-700"
: "border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800",
"block border-l-4 py-2 pl-3 pr-4 text-base font-medium"
)}
aria-current={item.current ? "page" : undefined}
onClick={() => {
close();
}}
>
}}>
{item.name}
</Link>
))}
@@ -259,8 +250,7 @@ export default function TopNavigation() {
: item.click
}
href={item.href}
className="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800"
>
className="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800">
{item.name}
</Link>
))}
+22 -45
View File
@@ -1,12 +1,12 @@
import { ChangeEvent, useEffect, useState } from "react";
import { KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import Link from "next/link";
import Head from "next/head";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { updateUser } from "@documenso/features";
import { Button } from "@documenso/ui";
import { getUser } from "@documenso/lib/api";
import { Button } from "@documenso/ui";
import { KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
import { useSession } from "next-auth/react";
const subNavigation = [
{
@@ -74,15 +74,12 @@ export default function Setttings() {
</Head>
<header className="py-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold leading-tight tracking-tight text-brown">
Settings
</h1>
<h1 className="text-brown text-3xl font-bold leading-tight tracking-tight">Settings</h1>
</div>
</header>
<div
className="mx-auto max-w-screen-xl px-4 pb-6 sm:px-6 lg:px-8 lg:pb-16"
hidden={!user.email}
>
hidden={!user.email}>
<div className="overflow-hidden rounded-lg bg-white shadow">
<div className="divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
<aside className="py-6 lg:col-span-3">
@@ -93,18 +90,17 @@ export default function Setttings() {
href={item.href}
className={classNames(
item.current
? "bg-teal-50 border-neon-dark text-teal-700 hover:bg-teal-50 hover:text-teal-700"
? "border-neon-dark bg-teal-50 text-teal-700 hover:bg-teal-50 hover:text-teal-700"
: "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900",
"group border-l-4 px-3 py-2 flex items-center text-sm font-medium"
"group flex items-center border-l-4 px-3 py-2 text-sm font-medium"
)}
aria-current={item.current ? "page" : undefined}
>
aria-current={item.current ? "page" : undefined}>
<item.icon
className={classNames(
item.current
? "text-teal-500 group-hover:text-teal-500"
: "text-gray-400 group-hover:text-gray-500",
"flex-shrink-0 -ml-1 mr-3 h-6 w-6"
"-ml-1 mr-3 h-6 w-6 flex-shrink-0"
)}
aria-hidden="true"
/>
@@ -115,20 +111,14 @@ export default function Setttings() {
</aside>
<form
className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9"
action="#"
method="POST"
hidden={
subNavigation.filter((e) => e.current)[0]?.name !==
subNavigation[0].name
}
>
hidden={subNavigation.filter((e) => e.current)[0]?.name !== subNavigation[0].name}>
{/* Profile section */}
<div className="py-6 px-4 sm:p-6 lg:pb-8">
<div>
<h2 className="text-lg font-medium leading-6 text-gray-900">
Profile
</h2>
<h2 className="text-lg font-medium leading-6 text-gray-900">Profile</h2>
<p className="mt-1 text-sm text-gray-500">
Let people know who they are dealing with builds trust.
</p>
@@ -136,10 +126,7 @@ export default function Setttings() {
<div className="my-6 grid grid-cols-12 gap-6">
<div className="col-span-12 sm:col-span-6">
<label
htmlFor="first-name"
className="block text-sm font-medium text-gray-700"
>
<label htmlFor="first-name" className="block text-sm font-medium text-gray-700">
Full Name
</label>
<input
@@ -150,14 +137,11 @@ export default function Setttings() {
onChange={(e) => handleNameChange(e)}
onKeyDown={handleKeyPress}
autoComplete="given-name"
className="mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:outline-none sm:text-sm"
/>
</div>
<div className="col-span-12 sm:col-span-6">
<label
htmlFor="first-name"
className="block text-sm font-medium text-gray-700"
>
<label htmlFor="first-name" className="block text-sm font-medium text-gray-700">
Email
</label>
<input
@@ -167,7 +151,7 @@ export default function Setttings() {
name="first-name"
id="first-name"
autoComplete="given-name"
className="mt-1 block w-full rounded-md border disabled:bg-neutral-100 border-gray-300 py-2 px-3 shadow-sm focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:outline-none disabled:bg-neutral-100 sm:text-sm"
/>
</div>
</div>
@@ -175,21 +159,14 @@ export default function Setttings() {
</div>
</form>
<div
hidden={
subNavigation.filter((e) => e.current)[0]?.name !==
subNavigation[1].name
}
className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
>
hidden={subNavigation.filter((e) => e.current)[0]?.name !== subNavigation[1].name}
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9">
{/* Passwords section */}
<div className="py-6 px-4 sm:p-6 lg:pb-8">
<div>
<h2 className="text-lg font-medium leading-6 text-gray-900">
Password
</h2>
<h2 className="text-lg font-medium leading-6 text-gray-900">Password</h2>
<p className="mt-1 text-sm text-gray-500">
Forgot your passwort? Email <b>hi@documenso.com</b> to reset
it.
Forgot your passwort? Email <b>hi@documenso.com</b> to reset it.
</p>
</div>
</div>
+9 -19
View File
@@ -1,9 +1,9 @@
import Link from "next/link";
import { signup } from "@documenso/lib/api";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { Button } from "@documenso/ui";
import { XCircleIcon } from "@heroicons/react/24/outline";
import { signIn } from "next-auth/react";
import Link from "next/link";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
@@ -107,8 +107,7 @@ export default function Signup(props: { source: string }) {
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="rgb(17 24 39 / var(--tw-text-opacity))"
className="w-8 h-8 inline mb-1"
>
className="mb-1 inline h-8 w-8">
<path
strokeLinecap="round"
strokeLinejoin="round"
@@ -130,8 +129,7 @@ export default function Signup(props: { source: string }) {
form.clearErrors();
trigger();
}}
className="mt-8 space-y-6"
>
className="mt-8 space-y-6">
<input type="hidden" name="remember" defaultValue="true" />
<div className="-space-y-px rounded-md shadow-sm">
<div>
@@ -145,7 +143,7 @@ export default function Signup(props: { source: string }) {
type="email"
autoComplete="email"
required
className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="Email"
/>
</div>
@@ -157,8 +155,7 @@ export default function Signup(props: { source: string }) {
{...register("password", {
minLength: {
value: 7,
message:
"Your password has to be at least 7 characters long.",
message: "Your password has to be at least 7 characters long.",
},
})}
id="password"
@@ -166,7 +163,7 @@ export default function Signup(props: { source: string }) {
type="password"
autoComplete="current-password"
required
className="relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="Password"
/>
</div>
@@ -177,16 +174,12 @@ export default function Signup(props: { source: string }) {
onClick={() => {
form.clearErrors();
}}
className="sgroup relative flex w-full"
>
className="sgroup relative flex w-full">
Create Account
</Button>
<div className="pt-2">
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center"></div>
@@ -194,10 +187,7 @@ export default function Signup(props: { source: string }) {
</div>
<p className="mt-2 text-center text-sm text-gray-600">
Already have an account?{" "}
<Link
href="/login"
className="font-medium text-neon hover:text-neon"
>
<Link href="/login" className="text-gray-500 hover:text-neon-700 font-medium">
Sign In
</Link>
</p>
+18
View File
@@ -0,0 +1,18 @@
import { useEffect, useState } from "react";
export function useDebouncedValue<T>(value: T, delay: number) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
+1 -2
View File
@@ -18,7 +18,6 @@ const withTM = require("next-transpile-modules")([
const plugins = [];
plugins.push(withTM);
const moduleExports = () =>
plugins.reduce((acc, next) => next(acc), nextConfig);
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
module.exports = moduleExports;
+7 -9
View File
@@ -1,31 +1,29 @@
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
import { Button } from "@documenso/ui";
import Logo from "../components/logo";
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
export default function Custom404() {
return (
<>
<main className="relative min-h-full bg-gray-100 isolate">
<main className="relative isolate min-h-full bg-gray-100">
<div className="absolute top-10 left-10">
<Logo className="w-10 md:w-20" />
</div>
<div className="px-6 py-48 mx-auto text-center max-w-7xl sm:py-40 lg:px-8">
<p className="text-base font-semibold leading-8 text-brown">404</p>
<h1 className="mt-4 text-3xl font-bold tracking-tight text-brown sm:text-5xl">
<div className="mx-auto max-w-7xl px-6 py-48 text-center sm:py-40 lg:px-8">
<p className="text-brown text-base font-semibold leading-8">404</p>
<h1 className="text-brown mt-4 text-3xl font-bold tracking-tight sm:text-5xl">
Page not found
</h1>
<p className="mt-4 text-base text-gray-700 sm:mt-6">
Sorry, we couldnt find the page youre looking for.
</p>
<div className="flex justify-center mt-10">
<div className="mt-10 flex justify-center">
<Button
color="secondary"
href="/"
icon={ArrowSmallLeftIcon}
className="text-base font-semibold leading-7 text-brown"
>
className="text-brown text-base font-semibold leading-7">
Back to home
</Button>
</div>
+6 -8
View File
@@ -1,27 +1,25 @@
import Logo from "../components/logo";
import { Button } from "@documenso/ui";
import Logo from "../components/logo";
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
import { EllipsisVerticalIcon } from "@heroicons/react/20/solid";
export default function Custom500() {
return (
<>
<div className="relative flex flex-col items-center justify-center min-h-full text-white bg-black">
<div className="relative flex min-h-full flex-col items-center justify-center bg-black text-white">
<div className="absolute top-10 left-10">
<Logo dark className="w-10 md:w-20" />
</div>
<div className="px-4 py-10 mt-20 max-w-7xl">
<div className="mt-20 max-w-7xl px-4 py-10">
<p className="inline-flex items-center text-3xl font-bold sm:text-5xl">
500
<span className="relative px-3 font-thin sm:text-6xl -top-1.5">
|
</span>{" "}
<span className="text-base font-semibold align-middle sm:text-2xl">
<span className="relative -top-1.5 px-3 font-thin sm:text-6xl">|</span>{" "}
<span className="align-middle text-base font-semibold sm:text-2xl">
Something went wrong.
</span>
</p>
<div className="flex justify-center mt-10">
<div className="mt-10 flex justify-center">
<Button color="secondary" href="/" icon={ArrowSmallLeftIcon}>
Back to home
</Button>
+7 -6
View File
@@ -1,13 +1,14 @@
import "../styles/tailwind.css";
import { ReactElement, ReactNode } from "react";
import { NextPage } from "next";
import type { AppProps } from "next/app";
import "../../../node_modules/placeholder-loading/src/scss/placeholder-loading.scss";
import "../../../node_modules/react-resizable/css/styles.css";
import "react-tooltip/dist/react-tooltip.css";
import { ReactElement, ReactNode } from "react";
import type { AppProps } from "next/app";
import { NextPage } from "next";
import "../styles/tailwind.css";
import { SessionProvider } from "next-auth/react";
export { coloredConsole } from "@documenso/lib";
import { Toaster } from "react-hot-toast";
import "react-tooltip/dist/react-tooltip.css";
export { coloredConsole } from "@documenso/lib";
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
+1 -4
View File
@@ -5,10 +5,7 @@ export default function Document(props) {
let pageProps = props.__NEXT_DATA__?.props?.pageProps;
return (
<Html
className="h-full bg-gray-100 scroll-smooth font-normal antialiased"
lang="en"
>
<Html className="h-full scroll-smooth bg-gray-100 font-normal antialiased" lang="en">
<Head>
<meta name="color-scheme"></meta>
</Head>
+6 -10
View File
@@ -1,9 +1,9 @@
import NextAuth, { Session } from "next-auth";
import GitHubProvider from "next-auth/providers/github";
import CredentialsProvider from "next-auth/providers/credentials";
import { ErrorCode } from "@documenso/lib/auth";
import prisma from "@documenso/prisma";
import { verifyPassword } from "@documenso/lib/auth";
import prisma from "@documenso/prisma";
import NextAuth, { Session } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GitHubProvider from "next-auth/providers/github";
export default NextAuth({
secret: process.env.AUTH_SECRET,
@@ -27,8 +27,7 @@ export default NextAuth({
password: {
label: "Password",
type: "password",
placeholder:
"Select a password. Here is some inspiration: https://xkcd.com/936/",
placeholder: "Select a password. Here is some inspiration: https://xkcd.com/936/",
},
},
async authorize(credentials: any) {
@@ -57,10 +56,7 @@ export default NextAuth({
throw new Error(ErrorCode.UserMissingPassword);
}
const isCorrectPassword = await verifyPassword(
credentials.password,
user.password
);
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
if (!isCorrectPassword) {
throw new Error(ErrorCode.IncorrectPassword);
+2 -3
View File
@@ -1,9 +1,8 @@
import { IdentityProvider } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@documenso/prisma";
import { hashPassword } from "@documenso/lib/auth";
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { IdentityProvider } from "@prisma/client";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { email, password, source } = req.body;
+6 -16
View File
@@ -1,13 +1,9 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { Document as PrismaDocument } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
import { Document as PrismaDocument } from "@prisma/client";
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const { id: documentId } = req.query;
@@ -46,8 +42,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
document = await getDocument(+documentId, req, res);
}
if (!document)
res.status(404).end(`No document with id ${documentId} found.`);
if (!document) res.status(404).end(`No document with id ${documentId} found.`);
const signaturesCount = await prisma.signature.count({
where: {
@@ -61,18 +56,13 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
// No need to add a signature, if no one signed yet.
if (signaturesCount > 0) {
signedDocumentAsBase64 = await addDigitalSignature(
document?.document || ""
);
signedDocumentAsBase64 = await addDigitalSignature(document?.document || "");
}
const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64");
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Length", buffer.length);
res.setHeader(
"Content-Disposition",
`attachment; filename=${document?.title}`
);
res.setHeader("Content-Disposition", `attachment; filename=${document?.title}`);
return res.status(200).send(buffer);
}
@@ -1,13 +1,9 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import short from "short-uuid";
import { Document as PrismaDocument, FieldType } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { FieldType, Document as PrismaDocument } from "@prisma/client";
import short from "short-uuid";
async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res);
@@ -1,12 +1,8 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { Document as PrismaDocument, FieldType } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { FieldType, Document as PrismaDocument } from "@prisma/client";
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res);
@@ -61,18 +57,14 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
});
if (!recipient || recipient?.documentId !== +documentId)
return res
.status(401)
.send("Recipient does not have access to this document.");
return res.status(401).send("Recipient does not have access to this document.");
}
if (user) {
const document: PrismaDocument = await getDocument(+documentId, req, res);
// todo entity ownerships checks
if (document.userId !== user.id) {
return res
.status(401)
.send("User does not have access to this document.");
return res.status(401).send("User does not have access to this document.");
}
}
@@ -1,13 +1,9 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import short from "short-uuid";
import { Document as PrismaDocument } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { Document as PrismaDocument } from "@prisma/client";
import short from "short-uuid";
async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res);
@@ -1,13 +1,9 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import short from "short-uuid";
import { Document as PrismaDocument } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { Document as PrismaDocument } from "@prisma/client";
import short from "short-uuid";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res);
+3 -8
View File
@@ -1,12 +1,8 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { sendSigningRequest } from "@documenso/lib/mail";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { Document as PrismaDocument, SendStatus } from "@prisma/client";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
@@ -23,8 +19,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const document: PrismaDocument = await getDocument(+documentId, req, res);
if (!document)
res.status(404).end(`No document with id ${documentId} found.`);
if (!document) res.status(404).end(`No document with id ${documentId} found.`);
let recipientCondition: any = {
documentId: +documentId,
+28 -22
View File
@@ -1,11 +1,11 @@
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { SigningStatus, DocumentStatus } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { Document as PrismaDocument, FieldType } from "@prisma/client";
import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
import { sendSigningDoneMail } from "@documenso/lib/mail";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
import prisma from "@documenso/prisma";
import { DocumentStatus, SigningStatus } from "@prisma/client";
import { FieldType, Document as PrismaDocument } from "@prisma/client";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { token: recipientToken } = req.query;
@@ -63,6 +63,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
},
data: {
signingStatus: SigningStatus.SIGNED,
signedAt: new Date(),
},
});
@@ -73,13 +74,24 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
},
});
const signedRecipients = await prisma.recipient.findMany({
where: {
documentId: recipient.documentId,
signingStatus: SigningStatus.SIGNED,
},
});
// Don't check for inserted, because currently no "sign again" scenarios exist and
// this is probably the expected behaviour in unclean states.
const nonSignatureFields = await prisma.field.findMany({
where: {
documentId: document.id,
type: { in: [FieldType.DATE, FieldType.TEXT] },
recipientId: { in: signedRecipients.map((r) => r.id) },
},
include: {
Recipient: true,
}
});
// Insert fields other than signatures
@@ -91,7 +103,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
month: "long",
day: "numeric",
year: "numeric",
}).format(new Date())
}).format(field.Recipient?.signedAt ?? new Date())
: field.customText || "",
field.positionX,
field.positionY,
@@ -115,10 +127,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
},
data: {
document: documentWithInserts,
status:
unsignedRecipients.length > 0
? DocumentStatus.PENDING
: DocumentStatus.COMPLETED,
status: unsignedRecipients.length > 0 ? DocumentStatus.PENDING : DocumentStatus.COMPLETED,
},
});
@@ -129,8 +138,11 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
});
document.document = documentWithInserts;
if (documentOwner)
await sendSigningDoneMail(recipient, document, documentOwner);
if (documentOwner) await sendSigningDoneMail(document, documentOwner);
for (const signer of signedRecipients) {
await sendSigningDoneMail(document, signer);
}
}
return res.status(200).end();
@@ -139,9 +151,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
if (signedField?.Signature?.signatureImageAsBase64) {
documentWithInserts = await insertImageInPDF(
documentWithInserts,
signedField.Signature
? signedField.Signature?.signatureImageAsBase64
: "",
signedField.Signature ? signedField.Signature?.signatureImageAsBase64 : "",
signedField.positionX,
signedField.positionY,
signedField.page
@@ -169,12 +179,8 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
create: {
recipientId: recipient.id,
fieldId: signature.fieldId,
signatureImageAsBase64: signature.signatureImage
? signature.signatureImage
: null,
typedSignature: signature.typedSignature
? signature.typedSignature
: null,
signatureImageAsBase64: signature.signatureImage ? signature.signatureImage : null,
typedSignature: signature.typedSignature ? signature.typedSignature : null,
},
});
}
+4 -4
View File
@@ -1,9 +1,9 @@
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { getUserFromToken } from "@documenso/lib/server";
import formidable from "formidable";
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import { getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import formidable from "formidable";
export const config = {
api: {
-1
View File
@@ -1,5 +1,4 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
+4 -6
View File
@@ -1,8 +1,9 @@
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import { NextApiRequest, NextApiResponse } from "next";
import { Document as PrismaDocument } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
import { Document as PrismaDocument } from "@prisma/client";
// todo remove before launch
@@ -12,10 +13,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const signedDocument = await addDigitalSignature(document.document);
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Length", signedDocument.length);
res.setHeader(
"Content-Disposition",
`attachment; filename=${document.title}`
);
res.setHeader("Content-Disposition", `attachment; filename=${document.title}`);
return res.status(200).send(signedDocument);
}
+2 -7
View File
@@ -1,11 +1,6 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import type { NextApiRequest, NextApiResponse } from "next";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { method, body } = req;
+2 -7
View File
@@ -1,11 +1,6 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import type { NextApiRequest, NextApiResponse } from "next";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res);
+21 -26
View File
@@ -1,7 +1,10 @@
import { ChangeEvent, ReactElement } from "react";
import Head from "next/head";
import { ReactElement } from "react";
import Layout from "../components/layout";
import Link from "next/link";
import { uploadDocument } from "@documenso/features";
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
import { getUserFromToken } from "@documenso/lib/server";
import Layout from "../components/layout";
import type { NextPageWithLayout } from "./_app";
import {
CheckBadgeIcon,
@@ -9,15 +12,12 @@ import {
ExclamationTriangleIcon,
UsersIcon,
} from "@heroicons/react/24/outline";
import { uploadDocument } from "@documenso/features";
import {
DocumentStatus,
Document as PrismaDocument,
SendStatus,
SigningStatus,
Document as PrismaDocument,
} from "@prisma/client";
import { getUserFromToken } from "@documenso/lib/server";
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
import { truncate } from "fs";
import { Tooltip as ReactTooltip } from "react-tooltip";
@@ -59,30 +59,30 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
Dashboard
</h1>
</header>
<dl className="grid gap-5 mt-8 md:grid-cols-3 ">
<dl className="mt-8 grid gap-5 md:grid-cols-3 ">
{stats.map((item) => (
<Link href={item.link} key={item.name}>
<div className="px-4 py-3 overflow-hidden bg-white rounded-lg shadow md:p-6 sm:py-5">
<dt className="text-sm font-medium text-gray-500 truncate ">
<div className="overflow-hidden rounded-lg bg-white px-4 py-3 shadow hover:shadow-lg duration-300 sm:py-5 md:p-6">
<dt className="truncate text-sm font-medium text-gray-700 ">
<item.icon
className="flex-shrink-0 inline w-5 h-5 mr-3 text-neon sm:w-6 sm:h-6"
aria-hidden="true"
></item.icon>
className="text-neon-600 mr-3 inline h-5 w-5 flex-shrink-0 sm:h-6 sm:w-6"
aria-hidden="true"></item.icon>
{item.name}
</dt>
<dd className="mt-1 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
<dd className="mt-3 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
{getStat(item.name, props)}
</dd>
</div>
</Link>
))}
</dl>
<div className="mt-12">
<input
id="fileUploadHelper"
type="file"
accept="application/pdf"
onChange={(event: any) => {
onChange={(event: ChangeEvent) => {
uploadDocument(event);
}}
hidden
@@ -92,25 +92,22 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
onClick={() => {
document?.getElementById("fileUploadHelper")?.click();
}}
className="relative block w-full p-12 text-center border-2 border-gray-300 border-dashed rounded-lg cursor-pointer hover:border-neon focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
className="group hover:border-neon-600 duration-200 relative block w-full cursor-pointer rounded-lg border-2 border-dashed border-gray-300 p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<svg
className="w-12 h-12 mx-auto text-gray-400"
className="mx-auto h-12 w-12 text-gray-400 group-hover:text-gray-700 duration-200"
stroke="currentColor"
fill="none"
viewBox="0 00 20 25"
aria-hidden="true"
>
aria-hidden="true">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
/>
</svg>
<span
id="add_document"
className="mt-2 block text-sm font-medium text-neon"
>
<span id="add_document" className="text-gray-500 group-hover:text-neon-700 mt-2 block text-sm font-medium duration-200">
Add a new PDF document.
</span>
</div>
@@ -147,9 +144,7 @@ export async function getServerSideProps(context: any) {
const documents: any[] = await getDocumentsForUserFromToken(context);
const drafts: PrismaDocument[] = documents.filter(
(d) => d.status === DocumentStatus.DRAFT
);
const drafts: PrismaDocument[] = documents.filter((d) => d.status === DocumentStatus.DRAFT);
const waiting: any[] = documents.filter(
(e) =>
+76 -110
View File
@@ -1,7 +1,12 @@
import { ReactElement, useEffect, useState } from "react";
import { NextPageContext } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import { uploadDocument } from "@documenso/features";
import { deleteDocument, getDocuments } from "@documenso/lib/api";
import { Button, IconButton, SelectBox } from "@documenso/ui";
import Layout from "../components/layout";
import type { NextPageWithLayout } from "./_app";
import Head from "next/head";
import {
ArrowDownTrayIcon,
CheckBadgeIcon,
@@ -13,12 +18,7 @@ import {
PlusIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import { uploadDocument } from "@documenso/features";
import { DocumentStatus } from "@prisma/client";
import { Button, IconButton, SelectBox } from "@documenso/ui";
import { NextPageContext } from "next";
import { deleteDocument, getDocuments } from "@documenso/lib/api";
import { Tooltip as ReactTooltip } from "react-tooltip";
const DocumentsPage: NextPageWithLayout = (props: any) => {
@@ -27,7 +27,13 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
const [filteredDocuments, setFilteredDocuments] = useState([]);
const [loading, setLoading] = useState(true);
const statusFilters = [
type statusFilterType = {
label: string;
value: DocumentStatus | "ALL";
};
const statusFilters: statusFilterType[] = [
{ label: "All", value: "ALL" },
{ label: "Draft", value: "DRAFT" },
{ label: "Waiting for others", value: "PENDING" },
@@ -42,12 +48,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
{ label: "Last 12 months", value: 366 },
];
const [selectedStatusFilter, setSelectedStatusFilter] = useState(
statusFilters[0]
);
const [selectedCreatedFilter, setSelectedCreatedFilter] = useState(
createdFilter[0]
);
const [selectedStatusFilter, setSelectedStatusFilter] = useState(statusFilters[0]);
const [selectedCreatedFilter, setSelectedCreatedFilter] = useState(createdFilter[0]);
const loadDocuments = async () => {
if (!documents.length) setLoading(true);
@@ -62,9 +64,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
useEffect(() => {
loadDocuments().finally(() => {
setSelectedStatusFilter(
statusFilters.filter(
(status) => status.value === props.filter.toUpperCase()
)[0]
statusFilters.filter((status) => status.value === props.filter.toUpperCase())[0]
);
});
}, []);
@@ -79,9 +79,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
function filterDocumentes(documents: []): any {
let filteredDocuments = documents.filter(
(d: any) =>
d.status === selectedStatusFilter.value ||
selectedStatusFilter.value === "ALL"
(d: any) => d.status === selectedStatusFilter.value || selectedStatusFilter.value === "ALL"
);
filteredDocuments = filteredDocuments.filter((document: any) =>
@@ -91,6 +89,20 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
return filteredDocuments;
}
function handleStatusFilterChange(status: statusFilterType) {
router.replace(
{
pathname: router.pathname,
query: { filter: status.value },
},
undefined,
{
shallow: true, // Perform a shallow update, without reloading the page
}
);
setSelectedStatusFilter(status);
}
function wasXDaysAgoOrLess(documentDate: Date, lastXDays: number): boolean {
if (lastXDays < 0) return true;
@@ -98,9 +110,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
const today: Date = new Date(); // Today's date
// Calculate the difference between the two dates in days
const diffInDays = Math.floor(
(today.getTime() - documentDate.getTime()) / millisecondsInDay
);
const diffInDays = Math.floor((today.getTime() - documentDate.getTime()) / millisecondsInDay);
console.log(diffInDays);
@@ -114,7 +124,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<title>Documents | Documenso</title>
</Head>
<div className="px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:items-center mt-10">
<div className="mt-10 sm:flex sm:items-center">
<div className="sm:flex-auto">
<header>
<h1 className="text-3xl font-bold leading-tight tracking-tight text-gray-900">
@@ -127,34 +137,31 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
icon={DocumentPlusIcon}
onClick={() => {
document?.getElementById("fileUploadHelper")?.click();
}}
>
}}>
Add Document
</Button>
</div>
</div>
<div className="mt-3 mb-12">
<div className="w-fit block float-right ml-3 mt-7">
{filteredDocuments.length != 1
? filteredDocuments.length + " Documents"
: "1 Document"}
<div className="mt-3 mb-12 flex flex-row-reverse items-center gap-x-4">
<div className="pt-5 block w-fit">
{filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
</div>
<SelectBox
className="w-1/4 block float-right"
className="block w-1/4"
label="Created"
options={createdFilter}
value={selectedCreatedFilter}
onChange={setSelectedCreatedFilter}
/>
<SelectBox
className="w-1/4 block float-right ml-3"
className="block w-1/4"
label="Status"
options={statusFilters}
value={selectedStatusFilter}
onChange={setSelectedStatusFilter}
onChange={handleStatusFilterChange}
/>
</div>
<div className="mt-20 max-w-[1100px]" hidden={!loading}>
<div className="mt-8 max-w-[1100px]" hidden={!loading}>
<div className="ph-item">
<div className="ph-col-12">
<div className="ph-picture"></div>
@@ -171,14 +178,10 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
</div>
</div>
</div>
<div
className="mt-28 flex flex-col"
hidden={!documents.length || loading}
>
<div className="mt-8 flex flex-col" hidden={!documents.length || loading}>
<div
className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8"
hidden={!documents.length || loading}
>
hidden={!documents.length || loading}>
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table className="min-w-full divide-y divide-gray-300">
@@ -186,32 +189,25 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<tr>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Title
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Recipients
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Status
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Created
</th>
<th
scope="col"
className="relative py-3.5 pl-3 pr-4 sm:pr-6"
>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">Delete</span>
</th>
</tr>
@@ -220,38 +216,30 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
{filteredDocuments.map((document: any, index: number) => (
<tr
key={document.id}
className="hover:bg-gray-100 cursor-pointer"
onClick={(event) => showDocument(document.id)}
>
className="cursor-pointer hover:bg-gray-100"
onClick={(event) => showDocument(document.id)}>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{document.title || "#" + document.id}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<td className="whitespace-nowrap inline-flex py-3 gap-x-2 gap-y-1 flex-wrap max-w-[250px] text-sm text-gray-500">
{document.Recipient.map((item: any) => (
<div key={item.id}>
{item.sendStatus === "NOT_SENT" ? (
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"
>
{item.name
? item.name + " <" + item.email + ">"
: item.email}
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
{item.name ? item.name + " <" + item.email + ">" : item.email}
</span>
) : (
""
)}
{item.sendStatus === "SENT" &&
item.readStatus !== "OPENED" ? (
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
<span id="sent_icon">
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800"
>
<EnvelopeIcon className="inline h-5 mr-1"></EnvelopeIcon>
{item.name
? item.name + " <" + item.email + ">"
: item.email}
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
<EnvelopeIcon className="mr-1 inline h-4"></EnvelopeIcon>
{item.name ? item.name + " <" + item.email + ">" : item.email}
</span>
</span>
) : (
@@ -262,13 +250,10 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<span id="read_icon">
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800"
>
<CheckIcon className="inline h-5 -mr-2"></CheckIcon>
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
{item.name
? item.name + " <" + item.email + ">"
: item.email}
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
<CheckIcon className="-mr-2 inline h-4"></CheckIcon>
<CheckIcon className="mr-1 inline h-4"></CheckIcon>
{item.name ? item.name + " <" + item.email + ">" : item.email}
</span>
</span>
) : (
@@ -276,8 +261,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
)}
{item.signingStatus === "SIGNED" ? (
<span id="signed_icon">
<span className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
<CheckBadgeIcon className="inline h-5 mr-1"></CheckBadgeIcon>{" "}
<span className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>{" "}
{item.email}
</span>
</span>
@@ -307,9 +292,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
{formatDocumentStatus(document.status)}
<p>
<small hidden={document.Recipient.length === 0}>
{document.Recipient.filter(
(r: any) => r.signingStatus === "SIGNED"
).length || 0}
{document.Recipient.filter((r: any) => r.signingStatus === "SIGNED")
.length || 0}
/{document.Recipient.length || 0}
</small>
</p>
@@ -327,6 +311,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
event.stopPropagation();
router.push("/documents/" + document.id);
}}
disabled={document.status === "COMPLETED"}
/>
<IconButton
icon={ArrowDownTrayIcon}
@@ -342,30 +327,20 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
onClick={(event: any) => {
event.preventDefault();
event.stopPropagation();
if (
confirm(
"Are you sure you want to delete this document"
)
) {
if (confirm("Are you sure you want to delete this document")) {
const documentsWithoutIndex = [...documents];
const removedItem: any =
documentsWithoutIndex.splice(index, 1);
const removedItem: any = documentsWithoutIndex.splice(index, 1);
setDocuments(documentsWithoutIndex);
deleteDocument(document.id)
.catch((err) => {
documentsWithoutIndex.splice(
index,
0,
removedItem
);
documentsWithoutIndex.splice(index, 0, removedItem);
setDocuments(documentsWithoutIndex);
})
.then(() => {
loadDocuments();
});
}
}}
></IconButton>
}}></IconButton>
<span className="sr-only">, {document.name}</span>
</div>
</td>
@@ -374,29 +349,21 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
</tbody>
</table>
</div>
<div
hidden={filteredDocuments.length > 0}
className="mx-auto w-fit mt-12 p-3"
>
<FunnelIcon className="w-5 inline mr-1 align-middle" /> Nothing
here. Maybe try a different filter.
<div hidden={filteredDocuments.length > 0} className="mx-auto mt-12 w-fit p-3">
<FunnelIcon className="mr-1 inline w-5 align-middle" /> Nothing here. Maybe try a
different filter.
</div>
</div>
</div>
</div>
</div>
<div
className="text-center mt-24"
id="empty"
hidden={documents.length > 0 || loading}
>
<div className="mt-24 text-center" id="empty" hidden={documents.length > 0 || loading}>
<svg
className="mx-auto h-12 w-12 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
aria-hidden="true">
<path
strokeLinecap="round"
strokeLinejoin="round"
@@ -413,8 +380,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
icon={PlusIcon}
onClick={() => {
document?.getElementById("fileUploadHelper")?.click();
}}
>
}}>
Add Document
</Button>
<input
+13 -30
View File
@@ -1,20 +1,16 @@
import { ReactElement } from "react";
import Layout from "../../../components/layout";
import { NextPageWithLayout } from "../../_app";
import Link from "next/link";
import { useRouter } from "next/router";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
import { getUserFromToken } from "@documenso/lib/server";
import Link from "next/link";
import { DocumentStatus } from "@prisma/client";
import {
InformationCircleIcon,
PaperAirplaneIcon,
UsersIcon,
} from "@heroicons/react/24/outline";
import { getDocument } from "@documenso/lib/query";
import { Document as PrismaDocument } from "@prisma/client";
import { Button, Breadcrumb } from "@documenso/ui";
import { getUserFromToken } from "@documenso/lib/server";
import { Breadcrumb, Button } from "@documenso/ui";
import PDFEditor from "../../../components/editor/pdf-editor";
import Layout from "../../../components/layout";
import { NextPageWithLayout } from "../../_app";
import { InformationCircleIcon, PaperAirplaneIcon, UsersIcon } from "@heroicons/react/24/outline";
import { DocumentStatus } from "@prisma/client";
import { Document as PrismaDocument } from "@prisma/client";
const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
const router = useRouter();
@@ -32,8 +28,7 @@ const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
},
{
title: props.document.title,
href:
NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
},
]}
/>
@@ -67,21 +62,13 @@ const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
<Button
icon={PaperAirplaneIcon}
className="ml-3"
href={
NEXT_PUBLIC_WEBAPP_URL +
"/documents/" +
props.document.id +
"/recipients"
}
href={NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients"}
onClick={() => {
if (
confirm(
`Send document out to ${props?.document?.Recipient?.length} recipients?`
)
confirm(`Send document out to ${props?.document?.Recipient?.length} recipients?`)
) {
}
}}
>
}}>
Prepare to Send
</Button>
</div>
@@ -120,11 +107,7 @@ export async function getServerSideProps(context: any) {
const { id: documentId } = context.query;
try {
const document: PrismaDocument = await getDocument(
+documentId,
context.req,
context.res
);
const document: PrismaDocument = await getDocument(+documentId, context.req, context.res);
return {
props: {
+135 -139
View File
@@ -1,31 +1,33 @@
import Head from "next/head";
import { ReactElement, useRef, useState } from "react";
import Head from "next/head";
import { NEXT_PUBLIC_WEBAPP_URL, classNames } from "@documenso/lib";
import { createOrUpdateRecipient, deleteRecipient, sendSigningRequests } from "@documenso/lib/api";
import { getDocument } from "@documenso/lib/query";
import { getUserFromToken } from "@documenso/lib/server";
import { Breadcrumb, Button, Dialog, IconButton } from "@documenso/ui";
import Layout from "../../../components/layout";
import { NextPageWithLayout } from "../../_app";
import { classNames, NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
import {
ArrowDownTrayIcon,
CheckBadgeIcon,
CheckIcon,
EnvelopeIcon,
PaperAirplaneIcon,
PencilSquareIcon,
TrashIcon,
UserPlusIcon,
EnvelopeIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { getUserFromToken } from "@documenso/lib/server";
import { getDocument } from "@documenso/lib/query";
import { Document as PrismaDocument, DocumentStatus } from "@prisma/client";
import { Breadcrumb, Button, Dialog, IconButton } from "@documenso/ui";
import { createOrUpdateRecipient, deleteRecipient, sendSigningRequests } from "@documenso/lib/api";
import { DocumentStatus, Document as PrismaDocument, Recipient } from "@prisma/client";
import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form";
import { toast } from "react-hot-toast";
export type FormValues = {
signers: { id: number; email: string; name: string }[];
signers: Array<Pick<Recipient, 'id' | 'email' | 'name' | 'sendStatus' | 'readStatus' | 'signingStatus'>>;
};
type FormSigner = FormValues["signers"][number];
const RecipientsPage: NextPageWithLayout = (props: any) => {
const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso";
const breadcrumbItems = [
@@ -35,7 +37,10 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
},
{
title: props.document.title,
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
href:
props.document.status !== DocumentStatus.COMPLETED
? NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id
: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients",
},
{
title: "Recipients",
@@ -61,7 +66,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
});
const formValues = useWatch({ control, name: "signers" });
const cancelButtonRef = useRef(null);
const hasEmailError = (formValue: any): boolean => {
const hasEmailError = (formValue: FormSigner): boolean => {
const index = formValues.findIndex((e) => e.id === formValue.id);
return !!errors?.signers?.[index]?.email;
};
@@ -71,80 +76,84 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
<Head>
<title>{title}</title>
</Head>
<div className="px-6 mt-10 sm:px-0">
<div className="mt-10 px-6 sm:px-0">
<div>
<Breadcrumb document={props.document} items={breadcrumbItems} />
</div>
<div className="mt-2 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<div className="min-w-0 flex-1">
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
{props.document.title}
</h2>
</div>
<div className="flex flex-shrink-0 mt-4 md:mt-0 md:ml-4">
<div className="mt-4 flex flex-shrink-0 md:mt-0 md:ml-4">
<Button
icon={ArrowDownTrayIcon}
color="secondary"
className="mr-2"
href={"/api/documents/" + props.document.id}
>
href={"/api/documents/" + props.document.id}>
Download
</Button>
<Button
icon={PencilSquareIcon}
disabled={props.document.status === DocumentStatus.COMPLETED}
color={props.document.status === DocumentStatus.COMPLETED ? "primary" : "secondary"}
className="mr-2"
href={breadcrumbItems[1].href}
>
Edit Document
</Button>
<Button
className="min-w-[125px]"
color="primary"
icon={PaperAirplaneIcon}
onClick={() => {
setOpen(true);
}}
disabled={
(formValues.length || 0) === 0 ||
!formValues.some(
(r: any) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
) ||
loading
}
>
Send
</Button>
{props.document.status !== DocumentStatus.COMPLETED && (
<>
<Button
icon={PencilSquareIcon}
disabled={props.document.status === DocumentStatus.COMPLETED}
color={
props.document.status === DocumentStatus.COMPLETED ? "primary" : "secondary"
}
className="mr-2"
href={breadcrumbItems[1].href}>
Edit Document
</Button>
<Button
className="min-w-[125px]"
color="primary"
icon={PaperAirplaneIcon}
onClick={() => {
formValues.some((r) => r.email && hasEmailError(r))
? toast.error("Please enter a valid email address.", { id: "invalid email" })
: setOpen(true);
}}
disabled={
(formValues.length || 0) === 0 ||
!formValues.some(
(r) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
) ||
loading
}>
Send
</Button>
</>
)}
</div>
</div>
<div className="p-4 mt-10 overflow-hidden bg-white rounded-md shadow sm:p-6">
<div className="pb-3 border-b border-gray-200 sm:pb-5">
<div className="mt-10 overflow-hidden rounded-md bg-white p-4 shadow sm:p-6">
<div className="border-b border-gray-200 pb-3 sm:pb-5">
<h3 className="text-lg font-medium leading-6 text-gray-900 ">Signers</h3>
<p className="max-w-4xl mt-2 text-sm text-gray-500">
The people who will sign the document.
<p className="mt-2 max-w-4xl text-sm text-gray-500">
{props.document.status !== DocumentStatus.COMPLETED
? "The people who will sign the document."
: "The people who signed the document."}
</p>
</div>
<FormProvider {...form}>
<form
onChange={() => {
trigger();
}}
>
}}>
<ul role="list" className="divide-y divide-gray-200">
{fields.map((item: any, index: number) => (
{fields.map((item, index) => (
<li
key={index}
className="w-full px-2 py-3 border-0 hover:bg-green-50 group sm:py-4"
>
className="group w-full border-0 px-2 py-3 hover:bg-green-50 sm:py-4">
<div id="container" className="block w-full lg:flex lg:justify-between">
<div className="block space-y-2 md:space-x-2 md:space-y-0 md:flex">
<div className="block space-y-2 md:flex md:space-x-2 md:space-y-0">
<div
className={classNames(
"md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
"focus-within:border-neon focus-within:ring-neon rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:ring-1 md:w-[250px]",
item.sendStatus === "SENT" ? "bg-gray-100" : ""
)}
>
)}>
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
Email
</label>
@@ -170,8 +179,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
documentId: props.document.id,
});
}}
className="block w-full p-0 text-gray-900 placeholder-gray-500 disabled:bg-neutral-100 border-0 outline-none sm:text-sm bg-inherit"
placeholder="john.dorian@loremipsum.com"
className="block w-full border-0 bg-inherit p-0 text-gray-900 placeholder-gray-500 outline-none disabled:bg-neutral-100 sm:text-sm"
/>
{errors?.signers?.[index] ? (
<p className="mt-2 text-sm text-red-600" id="email-error">
@@ -183,10 +191,9 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
</div>
<div
className={classNames(
"md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
"focus-within:border-neon focus-within:ring-neon rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:ring-1 md:w-[250px]",
item.sendStatus === "SENT" ? "bg-gray-100" : ""
)}
>
)}>
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
Name (optional)
</label>
@@ -209,121 +216,110 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
documentId: props.document.id,
});
}}
className="block w-full p-0 text-gray-900 placeholder-gray-500 disabled:bg-neutral-100 border-0 outline-none sm:text-sm bg-inherit"
placeholder="John Dorian"
className="block w-full border-0 bg-inherit p-0 text-gray-900 placeholder-gray-500 outline-none disabled:bg-neutral-100 sm:text-sm"
/>
</div>
</div>
<div className="flex items-center space-x-2 lg:ml-2">
<div className="flex mb-2 mr-2 lg:mr-0">
<div className="mb-2 mr-2 flex lg:mr-0">
<div key={item.id} className="space-x-2">
{item.sendStatus === "NOT_SENT" ? (
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800">
Not Sent
</span>
) : (
""
)}
) : null}
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
<span id="sent_icon">
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800 "
>
<CheckIcon className="inline h-5 mr-1" /> Sent
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800 ">
<CheckIcon className="mr-1 inline h-5" /> Sent
</span>
</span>
) : (
""
)}
) : null}
{item.readStatus === "OPENED" && item.signingStatus === "NOT_SIGNED" ? (
<span id="read_icon">
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
<CheckIcon className="inline h-5 -mr-2"></CheckIcon>
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800">
<CheckIcon className="-mr-2 inline h-5"></CheckIcon>
<CheckIcon className="mr-1 inline h-5"></CheckIcon>
Seen
</span>
</span>
) : (
""
)}
) : null}
{item.signingStatus === "SIGNED" ? (
<span id="signed_icon">
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"
>
<CheckBadgeIcon className="inline h-5 mr-1"></CheckBadgeIcon>
className="mt-3 inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>
Signed
</span>
</span>
) : (
""
)}
) : null}
</div>
</div>
<div className="flex mr-1">
<IconButton
icon={PaperAirplaneIcon}
disabled={
!item.id ||
item.sendStatus !== "SENT" ||
item.signingStatus === "SIGNED" ||
loading
}
color="secondary"
className="my-auto mr-4 h-9"
onClick={() => {
if (confirm("Resend this signing request?")) {
setLoading(true);
sendSigningRequests(props.document, [item.id]).finally(() => {
setLoading(false);
});
{props.document.status !== DocumentStatus.COMPLETED && (
<div className="mr-1 flex">
<IconButton
icon={PaperAirplaneIcon}
disabled={
!item.id ||
item.sendStatus !== "SENT" ||
item.signingStatus === "SIGNED" ||
loading
}
}}
>
Resend
</IconButton>
<IconButton
icon={TrashIcon}
disabled={!item.id || item.sendStatus === "SENT" || loading}
onClick={() => {
const removedItem = { ...fields }[index];
remove(index);
deleteRecipient(item)?.catch((err) => {
append(removedItem);
});
}}
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
/>
</div>
color="secondary"
className="my-auto mr-4 h-9"
onClick={() => {
if (confirm("Resend this signing request?")) {
setLoading(true);
sendSigningRequests(props.document, [item.id]).finally(() => {
setLoading(false);
});
}
}}>
Resend
</IconButton>
<IconButton
icon={TrashIcon}
disabled={!item.id || item.sendStatus === "SENT" || loading}
onClick={() => {
const removedItem = { ...fields }[index];
remove(index);
deleteRecipient(item)?.catch((err) => {
append(removedItem);
});
}}
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
/>
</div>
)}
</div>
</div>
</li>
))}
</ul>
<Button
icon={UserPlusIcon}
className="mt-3"
onClick={() => {
createOrUpdateRecipient({
id: "",
email: "",
name: "",
documentId: props.document.id,
}).then((res) => {
append(res);
});
}}
>
Add Signer
</Button>
{props.document.status !== "COMPLETED" && (
<Button
icon={UserPlusIcon}
className="mt-3"
onClick={() => {
createOrUpdateRecipient({
id: "",
email: "",
name: "",
documentId: props.document.id,
}).then((res) => {
append(res);
});
}}>
Add Signer
</Button>
)}
</form>
</FormProvider>
</div>
@@ -336,7 +332,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
open={open}
setLoading={setLoading}
setOpen={setOpen}
icon={<EnvelopeIcon className="w-6 h-6 text-green-600" aria-hidden="true" />}
icon={<EnvelopeIcon className="h-6 w-6 text-green-600" aria-hidden="true" />}
/>
</>
);
+15 -36
View File
@@ -1,11 +1,11 @@
import prisma from "@documenso/prisma";
import Head from "next/head";
import { NextPageWithLayout } from "../../_app";
import { ReadStatus } from "@prisma/client";
import PDFSigner from "../../../components/editor/pdf-signer";
import Link from "next/link";
import prisma from "@documenso/prisma";
import PDFSigner from "../../../components/editor/pdf-signer";
import { NextPageWithLayout } from "../../_app";
import { ClockIcon } from "@heroicons/react/24/outline";
import { FieldType, DocumentStatus } from "@prisma/client";
import { ReadStatus } from "@prisma/client";
import { DocumentStatus, FieldType } from "@prisma/client";
const SignPage: NextPageWithLayout = (props: any) => {
return (
@@ -14,36 +14,22 @@ const SignPage: NextPageWithLayout = (props: any) => {
<title>Sign | Documenso</title>
</Head>
{!props.expired ? (
<PDFSigner
document={props.document}
recipient={props.recipient}
fields={props.fields}
/>
<PDFSigner document={props.document} recipient={props.recipient} fields={props.fields} />
) : (
<>
<div className="mx-auto w-fit px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
<ClockIcon className="text-neon w-10 inline mr-1"></ClockIcon>
<h1 className="text-base font-medium text-neon inline align-middle">
Time flies.
</h1>
<p className="mt-2 text-4xl font-bold tracking-tight">
This signing link is expired.
</p>
<ClockIcon className="text-neon mr-1 inline w-10"></ClockIcon>
<h1 className="text-neon inline align-middle text-base font-medium">Time flies.</h1>
<p className="mt-2 text-4xl font-bold tracking-tight">This signing link is expired.</p>
<p className="mt-2 text-base text-gray-500">
Please ask{" "}
{props.document.User.name
? `${props.document.User.name}`
: `the sender`}{" "}
Please ask {props.document.User.name ? `${props.document.User.name}` : `the sender`}{" "}
to resend it.
</p>
<div className="mx-auto w-fit text-xl pt-20"></div>
<div className="mx-auto w-fit pt-20 text-xl"></div>
</div>
<div>
<div className="relative mx-96">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center"></div>
@@ -51,10 +37,7 @@ const SignPage: NextPageWithLayout = (props: any) => {
</div>
<p className="mt-4 text-center text-sm text-gray-600">
Want to send of your own?{" "}
<Link
href="/signup?source=expired"
className="font-medium text-neon hover:text-neon"
>
<Link href="/signup?source=expired" className="text-neon hover:text-neon font-medium">
Create your own Account
</Link>
</p>
@@ -118,13 +101,9 @@ export async function getServerSideProps(context: any) {
return {
props: {
recipient: JSON.parse(JSON.stringify(recipient)),
document: JSON.parse(
JSON.stringify({ ...recipient.Document, document: "" })
),
document: JSON.parse(JSON.stringify({ ...recipient.Document, document: "" })),
fields: JSON.parse(JSON.stringify(unsignedFields)),
expired: recipient.expired
? new Date(recipient.expired) < new Date()
: false,
expired: recipient.expired ? new Date(recipient.expired) < new Date() : false,
},
};
}
+13 -34
View File
@@ -1,10 +1,10 @@
import prisma from "@documenso/prisma";
import Head from "next/head";
import { NextPageWithLayout } from "../../_app";
import { ArrowDownTrayIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
import { Button, IconButton } from "@documenso/ui";
import Link from "next/link";
import { useRouter } from "next/router";
import prisma from "@documenso/prisma";
import { Button, IconButton } from "@documenso/ui";
import { NextPageWithLayout } from "../../_app";
import { ArrowDownTrayIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
const Signed: NextPageWithLayout = (props: any) => {
const router = useRouter();
@@ -18,29 +18,18 @@ const Signed: NextPageWithLayout = (props: any) => {
<title>Sign | Documenso</title>
</Head>
<div className="mx-auto w-fit px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
<CheckBadgeIcon className="text-neon w-10 inline mr-1"></CheckBadgeIcon>
<h1 className="text-base font-medium text-neon inline align-middle">
It's done!
</h1>
<CheckBadgeIcon className="text-neon mr-1 inline w-10"></CheckBadgeIcon>
<h1 className="text-neon inline align-middle text-base font-medium">It's done!</h1>
<p className="mt-2 text-4xl font-bold tracking-tight">
You signed "{props.document.title}"
</p>
<p
className="mt-2 text-base text-gray-500 max-w-sm"
hidden={allRecipientsSigned}
>
<p className="mt-2 max-w-sm text-base text-gray-500" hidden={allRecipientsSigned}>
You will be notfied when all recipients have signed.
</p>
<p
className="mt-2 text-base text-gray-500 max-w-sm"
hidden={!allRecipientsSigned}
>
<p className="mt-2 max-w-sm text-base text-gray-500" hidden={!allRecipientsSigned}>
All recipients signed.
</p>
<div
className="mx-auto w-fit text-xl pt-20"
hidden={!allRecipientsSigned}
>
<div className="mx-auto w-fit pt-20 text-xl" hidden={!allRecipientsSigned}>
<Button
icon={ArrowDownTrayIcon}
color="secondary"
@@ -48,23 +37,16 @@ const Signed: NextPageWithLayout = (props: any) => {
event.preventDefault();
event.stopPropagation();
router.push(
"/api/documents/" +
props.document.id +
"?token=" +
props.recipient.token
"/api/documents/" + props.document.id + "?token=" + props.recipient.token
);
}}
>
}}>
Download "{props.document.title}"
</Button>
</div>
</div>
<div>
<div className="relative mx-96">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center"></div>
@@ -72,10 +54,7 @@ const Signed: NextPageWithLayout = (props: any) => {
</div>
<p className="mt-4 text-center text-sm text-gray-600">
Want to send slick signing links like this one?{" "}
<Link
href="https://documenso.com"
className="font-medium text-neon hover:text-neon"
>
<Link href="https://documenso.com" className="text-neon hover:text-neon font-medium">
Hosted Documenso is coming soon
</Link>
</p>
+11
View File
@@ -1,4 +1,5 @@
import Head from "next/head";
import { getUserFromToken } from "@documenso/lib/server";
import Login from "../components/login";
export default function LoginPage(props: any) {
@@ -13,6 +14,16 @@ export default function LoginPage(props: any) {
}
export async function getServerSideProps(context: any) {
const user = await getUserFromToken(context.req, context.res);
if (user)
return {
redirect: {
source: "/login",
destination: "/dashboard",
permanent: false,
},
};
const ALLOW_SIGNUP = process.env.ALLOW_SIGNUP === "true";
return {
+1
View File
@@ -1,2 +1,3 @@
import SettingsPage from ".";
export default SettingsPage;
+1
View File
@@ -1,2 +1,3 @@
import SettingsPage from ".";
export default SettingsPage;
+1
View File
@@ -1,2 +1,3 @@
import SettingsPage from ".";
export default SettingsPage;
+11
View File
@@ -1,5 +1,6 @@
import { NextPageContext } from "next";
import Head from "next/head";
import { getUserFromToken } from "@documenso/lib/server";
import Signup from "../components/signup";
export default function SignupPage(props: { source: string }) {
@@ -22,6 +23,16 @@ export async function getServerSideProps(context: any) {
},
};
const user = await getUserFromToken(context.req, context.res);
if (user)
return {
redirect: {
source: "/signup",
destination: "/dashboard",
permanent: false,
},
};
const signupSource: string = context.query["source"];
return {
props: {
+1 -1
View File
@@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};
+4 -6
View File
@@ -24,9 +24,8 @@ body,
font-weight: 400;
font-display: swap;
src: url("/fonts/montserrat.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin */
@@ -36,7 +35,6 @@ body,
font-weight: 700;
font-display: swap;
src: url("/fonts/montserrat.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+42 -3
View File
@@ -16,9 +16,48 @@ module.exports = {
qwigley: ["Qwigley", "serif"],
},
colors: {
neon: "#37f095",
"neon-dark": "#2CC077",
brown: "#353434",
neon: {
DEFAULT: "#37F095",
50: "#E2FDF0",
100: "#CFFBE5",
200: "#A9F9D1",
300: "#83F6BD",
400: "#5DF3A9",
500: "#37F095",
600: "#11DE79",
700: "#0DAA5D",
800: "#097640",
900: "#054224",
950: "#032816",
},
"neon-dark": {
DEFAULT: "#2CC077",
50: "#B5EED2",
100: "#A5EAC8",
200: "#84E3B4",
300: "#62DBA0",
400: "#41D48B",
500: "#2CC077",
600: "#22925B",
700: "#17653E",
800: "#0D3722",
900: "#020906",
950: "#000000",
},
brown: {
DEFAULT: "#353434",
50: "#918F8F",
100: "#878585",
200: "#737171",
300: "#5E5C5C",
400: "#4A4848",
500: "#353434",
600: "#191818",
700: "#000000",
800: "#000000",
900: "#000000",
950: "#000000",
},
},
borderRadius: {
"4xl": "2rem",
+50
View File
@@ -0,0 +1,50 @@
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS production_deps
WORKDIR /app
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
# Copy our current monorepo
COPY . .
RUN npm ci --production
# Install dependencies only when needed
FROM base AS builder
WORKDIR /app
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
# Copy our current monorepo
COPY . .
RUN npm ci
RUN npm run build --workspaces
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=production_deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=production_deps --chown=nextjs:nodejs /app/package-lock.json ./package-lock.json
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/package.json ./package.json
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next ./.next
EXPOSE 3000
ENV PORT 3000
CMD ["npm", "run", "start"]
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
command -v docker >/dev/null 2>&1 || {
echo "Docker is not running. Please start Docker and try again."
exit 1
}
command -v jq >/dev/null 2>&1 || {
echo "jq is not installed. Please install jq and try again."
exit 1
}
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
APP_VERSION="$(jq -r '.version' "$MONOREPO_ROOT/apps/web/package.json")"
GIT_SHA="$(git rev-parse HEAD)"
echo "Building docker image for monorepo at $MONOREPO_ROOT"
echo "App version: $APP_VERSION"
echo "Git SHA: $GIT_SHA"
docker build -f "$SCRIPT_DIR/Dockerfile" \
--progress=plain \
-t "documentso:latest" \
-t "documenso:$GIT_SHA" \
-t "documenso:$APP_VERSION" \
"$MONOREPO_ROOT"
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
cd "$MONOREPO_ROOT"
npm ci
npm run db-migrate:dev
npm run dev
+19
View File
@@ -0,0 +1,19 @@
name: documenso
services:
database:
image: postgres:15
container_name: database
environment:
- POSTGRES_USER=documenso
- POSTGRES_PASSWORD=password
- POSTGRES_DB=documenso
ports:
- 54320:5432
inbucket:
image: inbucket/inbucket
container_name: mailserver
ports:
- 9000:9000
- 2500:2500
- 1100:1100
+40
View File
@@ -0,0 +1,40 @@
services:
database:
image: postgres:15
environment:
- POSTGRES_USER=documenso
- POSTGRES_PASSWORD=password
- POSTGRES_DB=documenso
ports:
- 5432:5432
inbucket:
image: inbucket/inbucket
ports:
- 9000:9000
- 2500:2500
- 1100:1100
documenso:
image: node:18
working_dir: /app
command: ./docker/compose-entrypoint.sh
depends_on:
- database
- inbucket
environment:
- DATABASE_URL=postgres://documenso:password@database:5432/documenso
- NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
- NEXTAUTH_SECRET=my-super-secure-secret
- NEXTAUTH_URL=http://localhost:3000
- SENDGRID_API_KEY=
- SMTP_MAIL_HOST=inbucket
- SMTP_MAIL_PORT=2500
- SMTP_MAIL_USER=username
- SMTP_MAIL_PASSWORD=password
- MAIL_FROM=admin@example.com
- ALLOW_SIGNUP=true
ports:
- 3000:3000
volumes:
- ../:/app
Submodule
+1
Submodule documenso added at 8039871ab1
+1443
View File
File diff suppressed because it is too large Load Diff
+13 -2
View File
@@ -2,12 +2,17 @@
"name": "documenso-monorepo",
"version": "0.0.0",
"scripts": {
"dev": "cd apps && cd web && next dev",
"dev": "npm run dev -w apps/web",
"build": "npm i && cd apps && cd web && npm i && next build",
"start": "cd apps && cd web && next start",
"db-migrate:dev": "prisma migrate dev",
"db-seed": "prisma db seed",
"db-studio": "prisma studio"
"db-studio": "prisma studio",
"docker:compose": "docker-compose -f ./docker/compose-without-app.yml",
"docker:compose-up": "npm run docker:compose -- up -d",
"docker:compose-down": "npm run docker:compose -- down",
"dx": "npm install && run-s docker:compose-up db-migrate:dev",
"d": "npm install && run-s docker:compose-up db-migrate:dev && npm run db-seed && npm run dev"
},
"workspaces": [
"apps/*",
@@ -43,5 +48,11 @@
"react-hot-toast": "^2.4.0",
"react-signature-canvas": "^1.0.6",
"typescript": "4.8.4"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.5"
}
}
+2 -4
View File
@@ -14,10 +14,8 @@ export const createField = (
if (newFieldX < 0) newFieldX = 0;
if (newFieldY < 0) newFieldY = 0;
if (newFieldX + fieldSize.width > rect.width)
newFieldX = rect.width - fieldSize.width;
if (newFieldY + fieldSize.height > rect.height)
newFieldY = rect.height - fieldSize.height;
if (newFieldX + fieldSize.width > rect.width) newFieldX = rect.width - fieldSize.width;
if (newFieldY + fieldSize.height > rect.height) newFieldY = rect.height - fieldSize.height;
const signatureField = {
id: -1,
+7 -4
View File
@@ -1,9 +1,10 @@
import router from "next/router";
import toast from "react-hot-toast";
import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants";
import toast from "react-hot-toast";
import { ChangeEvent } from "react";
export const uploadDocument = async (event: any) => {
if (event.target.files && event.target.files[0]) {
export const uploadDocument = async (event: ChangeEvent) => {
if (event.target instanceof HTMLInputElement && event.target?.files && event.target.files[0]) {
const body = new FormData();
const document = event.target.files[0];
const fileName: string = event.target.files[0].name;
@@ -12,8 +13,10 @@ export const uploadDocument = async (event: any) => {
toast.error("Non-PDF documents are not supported yet.");
return;
}
body.append("document", document || "");
const response: any = await toast
await toast
.promise(
fetch("/api/documents", {
method: "POST",
+7 -10
View File
@@ -6,16 +6,13 @@ export const deleteRecipient = (recipient: any) => {
}
return toast.promise(
fetch(
"/api/documents/" + recipient.documentId + "/recipients/" + recipient.id,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(recipient),
}
),
fetch("/api/documents/" + recipient.documentId + "/recipients/" + recipient.id, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(recipient),
}),
{
loading: "Deleting...",
success: "Deleted.",
+1 -1
View File
@@ -7,4 +7,4 @@ export { getDocuments } from "./getDocuments";
export { deleteDocument } from "./deleteDocument";
export { deleteRecipient } from "./deleteRecipient";
export { createOrUpdateRecipient } from "./createOrUpdateRecipient";
export { sendSigningRequests } from "./sendSigningRequests";
export { sendSigningRequests } from "./sendSigningRequests";
+1 -4
View File
@@ -1,9 +1,6 @@
import toast from "react-hot-toast";
export const sendSigningRequests = async (
document: any,
resendTo: number[] = []
) => {
export const sendSigningRequests = async (document: any, resendTo: number[] = []) => {
if (!document || !document.id) return;
try {
const sent = await toast.promise(
+1 -5
View File
@@ -1,11 +1,7 @@
import { useRouter } from "next/router";
import toast from "react-hot-toast";
export const signDocument = (
document: any,
signatures: any[],
token: string
): Promise<any> => {
export const signDocument = (document: any, signatures: any[], token: string): Promise<any> => {
const body = { documentId: document.id, signatures };
return toast.promise(
+7 -19
View File
@@ -1,12 +1,8 @@
import { compare, hash } from "bcryptjs";
import type { NextApiRequest } from "next";
import type { Session } from "next-auth";
import {
getSession as getSessionInner,
GetSessionParams,
} from "next-auth/react";
import { HttpError } from "@documenso/lib/server";
import { compare, hash } from "bcryptjs";
import type { Session } from "next-auth";
import { GetSessionParams, getSession as getSessionInner } from "next-auth/react";
export async function hashPassword(password: string) {
const hashedPassword = await hash(password, 12);
@@ -28,9 +24,7 @@ export function validPassword(password: string) {
return true;
}
export async function getSession(
options: GetSessionParams
): Promise<Session | null> {
export async function getSession(options: GetSessionParams): Promise<Session | null> {
const session = await getSessionInner(options);
// that these are equal are ensured in `[...nextauth]`'s callback
@@ -43,11 +37,7 @@ export function isPasswordValid(
breakdown: boolean,
strict?: boolean
): { caplow: boolean; num: boolean; min: boolean; admin_min: boolean };
export function isPasswordValid(
password: string,
breakdown?: boolean,
strict?: boolean
) {
export function isPasswordValid(password: string, breakdown?: boolean, strict?: boolean) {
let cap = false, // Has uppercase characters
low = false, // Has lowercase characters
num = false, // At least one number
@@ -63,8 +53,7 @@ export function isPasswordValid(
}
}
if (!breakdown)
return cap && low && num && min && (strict ? admin_min : true);
if (!breakdown) return cap && low && num && min && (strict ? admin_min : true);
let errors: Record<string, boolean> = { caplow: cap && low, num, min };
// Only return the admin key if strict mode is enabled.
@@ -79,8 +68,7 @@ type CtxOrReq =
export const ensureSession = async (ctxOrReq: CtxOrReq) => {
const session = await getSession(ctxOrReq);
if (!session?.user)
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
if (!session?.user) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
return session;
};
+8 -1
View File
@@ -1 +1,8 @@
export const NEXT_PUBLIC_WEBAPP_URL = process.env.NEXT_PUBLIC_WEBAPP_URL;
export const NEXT_PUBLIC_WEBAPP_URL =
process.env.IS_PULL_REQUEST === "true"
? process.env.RENDER_EXTERNAL_URL
: process.env.NEXT_PUBLIC_WEBAPP_URL;
console.log("IS_PULL_REQUEST:" + process.env.IS_PULL_REQUEST);
console.log("RENDER_EXTERNAL_URL:" + process.env.RENDER_EXTERNAL_URL);
console.log("NEXT_PUBLIC_WEBAPP_URL:" + process.env.NEXT_PUBLIC_WEBAPP_URL);
+4 -11
View File
@@ -1,13 +1,9 @@
import { sendMail } from "./sendMail";
import { signingCompleteTemplate } from "@documenso/lib/mail";
import { Document as PrismaDocument } from "@prisma/client";
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
import { sendMail } from "./sendMail";
import { Document as PrismaDocument } from "@prisma/client";
export const sendSigningDoneMail = async (
recipient: any,
document: PrismaDocument,
user: any
) => {
export const sendSigningDoneMail = async (document: PrismaDocument, user: any) => {
await sendMail(
user.email,
`Completed: "${document.title}"`,
@@ -15,10 +11,7 @@ export const sendSigningDoneMail = async (
[
{
filename: document.title,
content: Buffer.from(
await addDigitalSignature(document.document),
"base64"
),
content: Buffer.from(await addDigitalSignature(document.document), "base64"),
},
]
);
+4 -4
View File
@@ -1,8 +1,8 @@
import prisma from "@documenso/prisma";
import { sendMail } from "./sendMail";
import { SendStatus, ReadStatus, DocumentStatus } from "@prisma/client";
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { signingRequestTemplate } from "@documenso/lib/mail";
import prisma from "@documenso/prisma";
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { sendMail } from "./sendMail";
import { DocumentStatus, ReadStatus, SendStatus } from "@prisma/client";
export const sendSigningRequest = async (recipient: any, document: any, user: any) => {
const signingRequestMessage = user.name
+1 -1
View File
@@ -1,6 +1,6 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { Document as PrismaDocument } from "@prisma/client";
import { baseEmailTemplate } from "./baseTemplate";
import { Document as PrismaDocument } from "@prisma/client";
export const signingCompleteTemplate = (message: string) => {
const customContent = `
+3 -3
View File
@@ -1,6 +1,6 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { Document as PrismaDocument } from "@prisma/client";
import { baseEmailTemplate } from "./baseTemplate";
import { Document as PrismaDocument } from "@prisma/client";
export const signingRequestTemplate = (
message: string,
@@ -11,8 +11,8 @@ export const signingRequestTemplate = (
user: any
) => {
const customContent = `
<p style="margin: 30px;">
<a href="${ctaLink}" style="background-color: #37f095; color: white; border-color: transparent; border-width: 1px; border-radius: 0.375rem; font-size: 18px; padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; text-decoration: none; margin-top: 4px; margin-bottom: 4px;">
<p style="margin: 30px 0px; text-align: center">
<a href="${ctaLink}" style="background-color: #37f095; white-space: nowrap; color: white; border-color: transparent; border-width: 1px; border-radius: 0.375rem; font-size: 18px; padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; text-decoration: none; margin-top: 4px; margin-bottom: 4px;">
${ctaLabel}
</a>
</p>
@@ -1,9 +1,7 @@
import { getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
export const getDocumentsForUserFromToken = async (
context: any
): Promise<any> => {
export const getDocumentsForUserFromToken = async (context: any): Promise<any> => {
const user = await getUserFromToken(context.req, context.res);
if (!user) return Promise.reject("Invalid user or token.");
+20 -17
View File
@@ -1,24 +1,27 @@
import type { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
type Handlers = {
[method in "GET" | "POST" | "PATCH" | "PUT" | "DELETE"]?: Promise<{ default: NextApiHandler }>;
[method in "GET" | "POST" | "PATCH" | "PUT" | "DELETE"]?: Promise<{
default: NextApiHandler;
}>;
};
/** Allows us to split big API handlers by method */
export const defaultHandler = (handlers: Handlers) => async (req: NextApiRequest, res: NextApiResponse) => {
const handler = (await handlers[req.method as keyof typeof handlers])?.default;
// auto catch unsupported methods.
if (!handler) {
return res
.status(405)
.json({ message: `Method Not Allowed (Allow: ${Object.keys(handlers).join(",")})` });
}
export const defaultHandler =
(handlers: Handlers) => async (req: NextApiRequest, res: NextApiResponse) => {
const handler = (await handlers[req.method as keyof typeof handlers])?.default;
// auto catch unsupported methods.
if (!handler) {
return res.status(405).json({
message: `Method Not Allowed (Allow: ${Object.keys(handlers).join(",")})`,
});
}
try {
await handler(req, res);
return;
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Something went wrong" });
}
};
try {
await handler(req, res);
return;
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Something went wrong" });
}
};
-1
View File
@@ -1,5 +1,4 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerErrorFromUnknown } from "@documenso/lib/server";
type Handle<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>;
@@ -1,9 +1,5 @@
import {
PrismaClientKnownRequestError,
NotFoundError,
} from "@prisma/client/runtime";
import { HttpError } from "@documenso/lib/server";
import { NotFoundError, PrismaClientKnownRequestError } from "@prisma/client/runtime";
export function getServerErrorFromUnknown(cause: unknown): HttpError {
// Error was manually thrown and does not need to be parsed.
+2 -2
View File
@@ -1,6 +1,6 @@
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@documenso/prisma";
import { User as PrismaUser } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { signOut } from "next-auth/react";
@@ -26,7 +26,7 @@ export async function getUserFromToken(
});
if (!user) {
if (res) res.status(401).end();
if (res && res.status) res.status(401).end();
return null;
}
+7 -1
View File
@@ -5,7 +5,13 @@ export class HttpError<TCode extends number = number> extends Error {
public readonly url: string | undefined;
public readonly method: string | undefined;
constructor(opts: { url?: string; method?: string; message?: string; statusCode: TCode; cause?: Error }) {
constructor(opts: {
url?: string;
method?: string;
message?: string;
statusCode: TCode;
cause?: Error;
}) {
super(opts.message ?? `HTTP Error ${opts.statusCode} `);
Object.setPrototypeOf(this, HttpError.prototype);
+19 -10
View File
@@ -1,6 +1,6 @@
import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
import fontkit from "@pdf-lib/fontkit";
import * as fs from "fs";
import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
export async function insertTextInPDF(
pdfAsBase64: string,
@@ -12,27 +12,36 @@ export async function insertTextInPDF(
): Promise<string> {
const fontBytes = fs.readFileSync("public/fonts/Qwigley-Regular.ttf");
const existingPdfBytes = pdfAsBase64;
const pdfDoc = await PDFDocument.load(pdfAsBase64);
const pdfDoc = await PDFDocument.load(existingPdfBytes);
pdfDoc.registerFontkit(fontkit);
const customFont = await pdfDoc.embedFont(fontBytes);
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
const font = await pdfDoc.embedFont(useHandwritingFont ? fontBytes : StandardFonts.Helvetica);
const pages = pdfDoc.getPages();
const pdfPage = pages[page];
const textSize = useHandwritingFont ? 50 : 15;
const textWidth = customFont.widthOfTextAtSize(text, textSize);
const textHeight = customFont.heightAtSize(textSize);
const textWidth = font.widthOfTextAtSize(text, textSize);
const textHeight = font.heightAtSize(textSize);
const fieldSize = { width: 192, height: 64 };
const invertedYPosition = pdfPage.getHeight() - positionY - fieldSize.height;
// Because pdf-lib use a bottom-left coordinate system, we need to invert the y position
// we then center the text in the middle by adding half the height of the text
// plus the height of the field and divide the result by 2
const invertedYPosition =
pdfPage.getHeight() - positionY - (fieldSize.height + textHeight / 2) / 2;
// We center the text by adding the width of the field, subtracting the width of the text
// and dividing the result by 2
const centeredXPosition = positionX + (fieldSize.width - textWidth) / 2;
pdfPage.drawText(text, {
x: positionX,
x: centeredXPosition,
y: invertedYPosition,
size: textSize,
font: useHandwritingFont ? customFont : helveticaFont,
color: rgb(0, 0, 0),
font,
});
const pdfAsUint8Array = await pdfDoc.save();
+2 -2
View File
@@ -1,5 +1,5 @@
import { PrismaClient, Document, User } from "@prisma/client";
import { isENVProd } from "@documenso/lib"
import { isENVProd } from "@documenso/lib";
import { Document, PrismaClient, User } from "@prisma/client";
declare global {
var client: PrismaClient | undefined;
@@ -0,0 +1,5 @@
-- DropForeignKey
ALTER TABLE "Signature" DROP CONSTRAINT "Signature_recipientId_fkey";
-- AddForeignKey
ALTER TABLE "Signature" ADD CONSTRAINT "Signature_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Recipient" ADD COLUMN "signedAt" TIMESTAMP(3);
+2 -1
View File
@@ -92,6 +92,7 @@ model Recipient {
name String @default("") @db.VarChar(255)
token String
expired DateTime?
signedAt DateTime?
readStatus ReadStatus @default(NOT_OPENED)
signingStatus SigningStatus @default(NOT_SIGNED)
sendStatus SendStatus @default(NOT_SENT)
@@ -130,6 +131,6 @@ model Signature {
signatureImageAsBase64 String?
typedSignature String?
Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Restrict)
Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
Field Field @relation(fields: [fieldId], references: [id], onDelete: Restrict)
}
+4 -4
View File
@@ -1,7 +1,7 @@
import prisma from "@documenso/prisma";
import { hashPassword } from "@documenso/lib/auth";
import { IdentityProvider } from "@prisma/client";
import { coloredConsole } from "@documenso/lib";
import { hashPassword } from "@documenso/lib/auth";
import prisma from "@documenso/prisma";
import { IdentityProvider } from "@prisma/client";
async function createUser(userData: { email: string; password: string }) {
try {
@@ -24,7 +24,7 @@ async function createUser(userData: { email: string; password: string }) {
async function main() {
console.info("Start seeding...");
const password = "123456789";
const email = "example6@documenso.com";
const email = "example@documenso.com";
const user = await createUser({
email: email,
password: await hashPassword(password),
+3 -10
View File
@@ -1,17 +1,10 @@
import { PDFDocument, PDFHexString, PDFName, PDFNumber, PDFString } from "pdf-lib";
const fs = require("fs");
// Local copy of Node SignPDF because https://github.com/vbuch/node-signpdf/pull/187 was not published in NPM yet. Can be switched to npm packge.
const signer = require("./node-signpdf/dist/signpdf");
import {
PDFDocument,
PDFName,
PDFNumber,
PDFHexString,
PDFString,
} from "pdf-lib";
export const addDigitalSignature = async (
documentAsBase64: string
): Promise<string> => {
export const addDigitalSignature = async (documentAsBase64: string): Promise<string> => {
// Custom code to add Byterange to PDF
const PDFArrayCustom = require("./PDFArrayCustom");
const pdfBuffer = Buffer.from(documentAsBase64, "base64");
+1 -1
View File
@@ -1 +1 @@
export { signDocument } from "./signDocument";
export { signDocument } from "./signDocument";
+8 -5
View File
@@ -1,9 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = exports.ERROR_VERIFY_SIGNATURE = exports.ERROR_TYPE_UNKNOWN = exports.ERROR_TYPE_PARSE = exports.ERROR_TYPE_INPUT = void 0;
exports.default =
exports.ERROR_VERIFY_SIGNATURE =
exports.ERROR_TYPE_UNKNOWN =
exports.ERROR_TYPE_PARSE =
exports.ERROR_TYPE_INPUT =
void 0;
const ERROR_TYPE_UNKNOWN = 1;
exports.ERROR_TYPE_UNKNOWN = ERROR_TYPE_UNKNOWN;
const ERROR_TYPE_INPUT = 2;
@@ -18,13 +23,11 @@ class SignPdfError extends Error {
super(msg);
this.type = type;
}
} // Shorthand
SignPdfError.TYPE_UNKNOWN = ERROR_TYPE_UNKNOWN;
SignPdfError.TYPE_INPUT = ERROR_TYPE_INPUT;
SignPdfError.TYPE_PARSE = ERROR_TYPE_PARSE;
SignPdfError.VERIFY_SIGNATURE = ERROR_VERIFY_SIGNATURE;
var _default = SignPdfError;
exports.default = _default;
exports.default = _default;
+14 -8
View File
@@ -1,18 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.SUBFILTER_ETSI_CADES_DETACHED = exports.SUBFILTER_ADOBE_X509_SHA1 = exports.SUBFILTER_ADOBE_PKCS7_SHA1 = exports.SUBFILTER_ADOBE_PKCS7_DETACHED = exports.DEFAULT_SIGNATURE_LENGTH = exports.DEFAULT_BYTE_RANGE_PLACEHOLDER = void 0;
exports.SUBFILTER_ETSI_CADES_DETACHED =
exports.SUBFILTER_ADOBE_X509_SHA1 =
exports.SUBFILTER_ADOBE_PKCS7_SHA1 =
exports.SUBFILTER_ADOBE_PKCS7_DETACHED =
exports.DEFAULT_SIGNATURE_LENGTH =
exports.DEFAULT_BYTE_RANGE_PLACEHOLDER =
void 0;
const DEFAULT_SIGNATURE_LENGTH = 8192;
exports.DEFAULT_SIGNATURE_LENGTH = DEFAULT_SIGNATURE_LENGTH;
const DEFAULT_BYTE_RANGE_PLACEHOLDER = '**********';
const DEFAULT_BYTE_RANGE_PLACEHOLDER = "**********";
exports.DEFAULT_BYTE_RANGE_PLACEHOLDER = DEFAULT_BYTE_RANGE_PLACEHOLDER;
const SUBFILTER_ADOBE_PKCS7_DETACHED = 'adbe.pkcs7.detached';
const SUBFILTER_ADOBE_PKCS7_DETACHED = "adbe.pkcs7.detached";
exports.SUBFILTER_ADOBE_PKCS7_DETACHED = SUBFILTER_ADOBE_PKCS7_DETACHED;
const SUBFILTER_ADOBE_PKCS7_SHA1 = 'adbe.pkcs7.sha1';
const SUBFILTER_ADOBE_PKCS7_SHA1 = "adbe.pkcs7.sha1";
exports.SUBFILTER_ADOBE_PKCS7_SHA1 = SUBFILTER_ADOBE_PKCS7_SHA1;
const SUBFILTER_ADOBE_X509_SHA1 = 'adbe.x509.rsa.sha1';
const SUBFILTER_ADOBE_X509_SHA1 = "adbe.x509.rsa.sha1";
exports.SUBFILTER_ADOBE_X509_SHA1 = SUBFILTER_ADOBE_X509_SHA1;
const SUBFILTER_ETSI_CADES_DETACHED = 'ETSI.CAdES.detached';
exports.SUBFILTER_ETSI_CADES_DETACHED = SUBFILTER_ETSI_CADES_DETACHED;
const SUBFILTER_ETSI_CADES_DETACHED = "ETSI.CAdES.detached";
exports.SUBFILTER_ETSI_CADES_DETACHED = SUBFILTER_ETSI_CADES_DETACHED;
@@ -1,13 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
var _SignPdfError = _interopRequireDefault(require("../SignPdfError"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const getSubstringIndex = (str, substring, n) => {
let times = 0;
@@ -30,42 +32,55 @@ const getSubstringIndex = (str, substring, n) => {
* @returns {Object} {ByteRange: Number[], signature: Buffer, signedData: Buffer}
*/
const extractSignature = (pdf, signatureCount = 1) => {
if (!(pdf instanceof Buffer)) {
throw new _SignPdfError.default('PDF expected as Buffer.', _SignPdfError.default.TYPE_INPUT);
throw new _SignPdfError.default("PDF expected as Buffer.", _SignPdfError.default.TYPE_INPUT);
} // const byteRangePos = pdf.indexOf('/ByteRange [');
const byteRangePos = getSubstringIndex(pdf, '/ByteRange [', signatureCount);
const byteRangePos = getSubstringIndex(pdf, "/ByteRange [", signatureCount);
if (byteRangePos === -1) {
throw new _SignPdfError.default('Failed to locate ByteRange.', _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
"Failed to locate ByteRange.",
_SignPdfError.default.TYPE_PARSE
);
}
const byteRangeEnd = pdf.indexOf(']', byteRangePos);
const byteRangeEnd = pdf.indexOf("]", byteRangePos);
if (byteRangeEnd === -1) {
throw new _SignPdfError.default('Failed to locate the end of the ByteRange.', _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
"Failed to locate the end of the ByteRange.",
_SignPdfError.default.TYPE_PARSE
);
}
const byteRange = pdf.slice(byteRangePos, byteRangeEnd + 1).toString();
const matches = /\/ByteRange \[(\d+) +(\d+) +(\d+) +(\d+) *\]/.exec(byteRange);
if (matches === null) {
throw new _SignPdfError.default('Failed to parse the ByteRange.', _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
"Failed to parse the ByteRange.",
_SignPdfError.default.TYPE_PARSE
);
}
const ByteRange = matches.slice(1).map(Number);
const signedData = Buffer.concat([pdf.slice(ByteRange[0], ByteRange[0] + ByteRange[1]), pdf.slice(ByteRange[2], ByteRange[2] + ByteRange[3])]);
const signatureHex = pdf.slice(ByteRange[0] + ByteRange[1] + 1, ByteRange[2]).toString('binary').replace(/(?:00|>)+$/, '');
const signature = Buffer.from(signatureHex, 'hex').toString('binary');
const signedData = Buffer.concat([
pdf.slice(ByteRange[0], ByteRange[0] + ByteRange[1]),
pdf.slice(ByteRange[2], ByteRange[2] + ByteRange[3]),
]);
const signatureHex = pdf
.slice(ByteRange[0] + ByteRange[1] + 1, ByteRange[2])
.toString("binary")
.replace(/(?:00|>)+$/, "");
const signature = Buffer.from(signatureHex, "hex").toString("binary");
return {
ByteRange: matches.slice(1, 5).map(Number),
signature,
signedData
signedData,
};
};
var _default = extractSignature;
exports.default = _default;
exports.default = _default;
+19 -10
View File
@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
@@ -9,7 +9,9 @@ var _SignPdfError = _interopRequireDefault(require("../SignPdfError"));
var _const = require("./const");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
/**
* Finds ByteRange information within a given PDF Buffer if one exists
@@ -17,25 +19,32 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
* @param {Buffer} pdf
* @returns {Object} {byteRangePlaceholder: String, byteRangeStrings: String[], byteRange: String[]}
*/
const findByteRange = pdf => {
const findByteRange = (pdf) => {
if (!(pdf instanceof Buffer)) {
throw new _SignPdfError.default('PDF expected as Buffer.', _SignPdfError.default.TYPE_INPUT);
throw new _SignPdfError.default("PDF expected as Buffer.", _SignPdfError.default.TYPE_INPUT);
}
const byteRangeStrings = pdf.toString().match(/\/ByteRange\s*\[{1}\s*(?:(?:\d*|\/\*{10})\s+){3}(?:\d+|\/\*{10}){1}\s*]{1}/g);
const byteRangeStrings = pdf
.toString()
.match(/\/ByteRange\s*\[{1}\s*(?:(?:\d*|\/\*{10})\s+){3}(?:\d+|\/\*{10}){1}\s*]{1}/g);
if (!byteRangeStrings) {
throw new _SignPdfError.default('No ByteRangeStrings found within PDF buffer', _SignPdfError.default.TYPE_PARSE);
throw new _SignPdfError.default(
"No ByteRangeStrings found within PDF buffer",
_SignPdfError.default.TYPE_PARSE
);
}
const byteRangePlaceholder = byteRangeStrings.find(s => s.includes(`/${_const.DEFAULT_BYTE_RANGE_PLACEHOLDER}`));
const byteRanges = byteRangeStrings.map(brs => brs.match(/[^[\s]*(?:\d|\/\*{10})/g));
const byteRangePlaceholder = byteRangeStrings.find((s) =>
s.includes(`/${_const.DEFAULT_BYTE_RANGE_PLACEHOLDER}`)
);
const byteRanges = byteRangeStrings.map((brs) => brs.match(/[^[\s]*(?:\d|\/\*{10})/g));
return {
byteRangePlaceholder,
byteRangeStrings,
byteRanges
byteRanges,
};
};
var _default = findByteRange;
exports.default = _default;
exports.default = _default;
+10 -8
View File
@@ -1,37 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
Object.defineProperty(exports, "extractSignature", {
enumerable: true,
get: function () {
return _extractSignature.default;
}
},
});
Object.defineProperty(exports, "findByteRange", {
enumerable: true,
get: function () {
return _findByteRange.default;
}
},
});
Object.defineProperty(exports, "pdfkitAddPlaceholder", {
enumerable: true,
get: function () {
return _pdfkitAddPlaceholder.default;
}
},
});
Object.defineProperty(exports, "plainAddPlaceholder", {
enumerable: true,
get: function () {
return _plainAddPlaceholder.default;
}
},
});
Object.defineProperty(exports, "removeTrailingNewLine", {
enumerable: true,
get: function () {
return _removeTrailingNewLine.default;
}
},
});
var _extractSignature = _interopRequireDefault(require("./extractSignature"));
@@ -44,6 +44,8 @@ var _removeTrailingNewLine = _interopRequireDefault(require("./removeTrailingNew
var _findByteRange = _interopRequireDefault(require("./findByteRange"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
'This string is added so that jest collects coverage for this file'; // eslint-disable-line
("This string is added so that jest collects coverage for this file"); // eslint-disable-line
@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
@@ -17,10 +17,9 @@ PDFAbstractReference - abstract class for PDF reference
*/
class PDFAbstractReference {
toString() {
throw new Error('Must be implemented by subclasses');
throw new Error("Must be implemented by subclasses");
}
}
var _default = PDFAbstractReference;
exports.default = _default;
exports.default = _default;
@@ -1,13 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
var _abstract_reference = _interopRequireDefault(require("./abstract_reference"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
/*
PDFObject by Devon Govett used below.
@@ -20,26 +22,26 @@ Modifications may have been applied for the purposes of node-signpdf.
PDFObject - converts JavaScript types into their corresponding PDF types.
By Devon Govett
*/
const pad = (str, length) => (Array(length + 1).join('0') + str).slice(-length);
const pad = (str, length) => (Array(length + 1).join("0") + str).slice(-length);
const escapableRe = /[\n\r\t\b\f()\\]/g;
const escapable = {
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\b': '\\b',
'\f': '\\f',
'\\': '\\\\',
'(': '\\(',
')': '\\)'
"\n": "\\n",
"\r": "\\r",
"\t": "\\t",
"\b": "\\b",
"\f": "\\f",
"\\": "\\\\",
"(": "\\(",
")": "\\)",
}; // Convert little endian UTF-16 to big endian
const swapBytes = buff => buff.swap16();
const swapBytes = (buff) => buff.swap16();
class PDFObject {
static convert(object, encryptFn = null) {
// String literals are converted to the PDF name type
if (typeof object === 'string') {
if (typeof object === "string") {
return `/${object}`; // String objects are converted to PDF strings (UTF-16)
}
@@ -55,29 +57,26 @@ class PDFObject {
}
} // If so, encode it as big endian UTF-16
let stringBuffer;
if (isUnicode) {
stringBuffer = swapBytes(Buffer.from(`\ufeff${string}`, 'utf16le'));
stringBuffer = swapBytes(Buffer.from(`\ufeff${string}`, "utf16le"));
} else {
stringBuffer = Buffer.from(string, 'ascii');
stringBuffer = Buffer.from(string, "ascii");
} // Encrypt the string when necessary
if (encryptFn) {
string = encryptFn(stringBuffer).toString('binary');
string = encryptFn(stringBuffer).toString("binary");
} else {
string = stringBuffer.toString('binary');
string = stringBuffer.toString("binary");
} // Escape characters as required by the spec
string = string.replace(escapableRe, c => escapable[c]);
string = string.replace(escapableRe, (c) => escapable[c]);
return `(${string})`; // Buffers are converted to PDF hex strings
}
if (Buffer.isBuffer(object)) {
return `<${object.toString('hex')}>`;
return `<${object.toString("hex")}>`;
}
if (object instanceof _abstract_reference.default) {
@@ -85,51 +84,57 @@ class PDFObject {
}
if (object instanceof Date) {
let string = `D:${pad(object.getUTCFullYear(), 4)}${pad(object.getUTCMonth() + 1, 2)}${pad(object.getUTCDate(), 2)}${pad(object.getUTCHours(), 2)}${pad(object.getUTCMinutes(), 2)}${pad(object.getUTCSeconds(), 2)}Z`; // Encrypt the string when necessary
let string = `D:${pad(object.getUTCFullYear(), 4)}${pad(object.getUTCMonth() + 1, 2)}${pad(
object.getUTCDate(),
2
)}${pad(object.getUTCHours(), 2)}${pad(object.getUTCMinutes(), 2)}${pad(
object.getUTCSeconds(),
2
)}Z`; // Encrypt the string when necessary
if (encryptFn) {
string = encryptFn(Buffer.from(string, 'ascii')).toString('binary'); // Escape characters as required by the spec
string = encryptFn(Buffer.from(string, "ascii")).toString("binary"); // Escape characters as required by the spec
string = string.replace(escapableRe, c => escapable[c]);
string = string.replace(escapableRe, (c) => escapable[c]);
}
return `(${string})`;
}
if (Array.isArray(object)) {
const items = object.map(e => PDFObject.convert(e, encryptFn)).join(' ');
const items = object.map((e) => PDFObject.convert(e, encryptFn)).join(" ");
return `[${items}]`;
}
if ({}.toString.call(object) === '[object Object]') {
const out = ['<<'];
if ({}.toString.call(object) === "[object Object]") {
const out = ["<<"];
let streamData; // @todo this can probably be refactored into a reduce
Object.entries(object).forEach(([key, val]) => {
let checkedValue = '';
let checkedValue = "";
if (val.toString().indexOf('<<') !== -1) {
if (val.toString().indexOf("<<") !== -1) {
checkedValue = val;
} else {
checkedValue = PDFObject.convert(val, encryptFn);
}
if (key === 'stream') {
if (key === "stream") {
streamData = `${key}\n${val}\nendstream`;
} else {
out.push(`/${key} ${checkedValue}`);
}
});
out.push('>>');
out.push(">>");
if (streamData) {
out.push(streamData);
}
return out.join('\n');
return out.join("\n");
}
if (typeof object === 'number') {
if (typeof object === "number") {
return PDFObject.number(object);
}
@@ -143,7 +148,6 @@ class PDFObject {
throw new Error(`unsupported number: ${n}`);
}
}
exports.default = PDFObject;
exports.default = PDFObject;
@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
@@ -9,7 +9,9 @@ var _const = require("./const");
var _pdfkitReferenceMock = _interopRequireDefault(require("./pdfkitReferenceMock"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
// eslint-disable-next-line import/no-unresolved
@@ -25,18 +27,18 @@ const pdfkitAddPlaceholder = ({
pdf,
pdfBuffer,
reason,
contactInfo = 'emailfromp1289@gmail.com',
name = 'Name from p12',
location = 'Location from p12',
contactInfo = "emailfromp1289@gmail.com",
name = "Name from p12",
location = "Location from p12",
signatureLength = _const.DEFAULT_SIGNATURE_LENGTH,
byteRangePlaceholder = _const.DEFAULT_BYTE_RANGE_PLACEHOLDER,
subFilter = _const.SUBFILTER_ADOBE_PKCS7_DETACHED
subFilter = _const.SUBFILTER_ADOBE_PKCS7_DETACHED,
}) => {
/* eslint-disable no-underscore-dangle,no-param-reassign */
// Generate the signature placeholder
const signature = pdf.ref({
Type: 'Sig',
Filter: 'Adobe.PPKLite',
Type: "Sig",
Filter: "Adobe.PPKLite",
SubFilter: subFilter,
ByteRange: [0, byteRangePlaceholder, byteRangePlaceholder, byteRangePlaceholder],
Contents: Buffer.from(String.fromCharCode(0).repeat(signatureLength)),
@@ -47,11 +49,10 @@ const pdfkitAddPlaceholder = ({
// eslint-disable-line no-new-wrappers
Name: new String(name),
// eslint-disable-line no-new-wrappers
Location: new String(location) // eslint-disable-line no-new-wrappers
Location: new String(location), // eslint-disable-line no-new-wrappers
}); // Check if pdf already contains acroform field
const acroFormPosition = pdfBuffer.lastIndexOf('/Type /AcroForm');
const acroFormPosition = pdfBuffer.lastIndexOf("/Type /AcroForm");
const isAcroFormExists = acroFormPosition !== -1;
let fieldIds = [];
let acroFormId;
@@ -65,13 +66,13 @@ const pdfkitAddPlaceholder = ({
// (generally it's 2 or 3, but I'm giving a big space though)
const maxAcroFormIdLength = 12;
let foundAcroFormId = '';
let foundAcroFormId = "";
let index = charsUntilIdEnd + 1;
for (index; index < charsUntilIdEnd + maxAcroFormIdLength; index += 1) {
const acroFormIdString = pdfBuffer.slice(acroFormPosition - index, acroFormIdEnd).toString();
if (acroFormIdString[0] === '\n') {
if (acroFormIdString[0] === "\n") {
break;
}
@@ -80,25 +81,27 @@ const pdfkitAddPlaceholder = ({
}
const pdfSlice = pdfBuffer.slice(acroFormStart);
const acroForm = pdfSlice.slice(0, pdfSlice.indexOf('endobj')).toString();
const acroForm = pdfSlice.slice(0, pdfSlice.indexOf("endobj")).toString();
acroFormId = parseInt(foundAcroFormId);
const acroFormFields = acroForm.slice(acroForm.indexOf('/Fields [') + 9, acroForm.indexOf(']'));
fieldIds = acroFormFields.split(' ').filter((element, i) => i % 3 === 0).map(fieldId => new _pdfkitReferenceMock.default(fieldId));
const acroFormFields = acroForm.slice(acroForm.indexOf("/Fields [") + 9, acroForm.indexOf("]"));
fieldIds = acroFormFields
.split(" ")
.filter((element, i) => i % 3 === 0)
.map((fieldId) => new _pdfkitReferenceMock.default(fieldId));
}
const signatureName = 'Signature'; // Generate signature annotation widget
const signatureName = "Signature"; // Generate signature annotation widget
const widget = pdf.ref({
Type: 'Annot',
Subtype: 'Widget',
FT: 'Sig',
Type: "Annot",
Subtype: "Widget",
FT: "Sig",
Rect: [0, 0, 0, 0],
V: signature,
T: new String(signatureName + (fieldIds.length + 1)),
// eslint-disable-line no-new-wrappers
F: 4,
P: pdf.page.dictionary // eslint-disable-line no-underscore-dangle
P: pdf.page.dictionary, // eslint-disable-line no-underscore-dangle
});
pdf.page.dictionary.data.Annots = [widget]; // Include the widget in a page
@@ -107,27 +110,30 @@ const pdfkitAddPlaceholder = ({
if (!isAcroFormExists) {
// Create a form (with the widget) and link in the _root
form = pdf.ref({
Type: 'AcroForm',
Type: "AcroForm",
SigFlags: 3,
Fields: [...fieldIds, widget]
Fields: [...fieldIds, widget],
});
} else {
// Use existing acroform and extend the fields with newly created widgets
form = pdf.ref({
Type: 'AcroForm',
SigFlags: 3,
Fields: [...fieldIds, widget]
}, acroFormId);
form = pdf.ref(
{
Type: "AcroForm",
SigFlags: 3,
Fields: [...fieldIds, widget],
},
acroFormId
);
}
pdf._root.data.AcroForm = form;
return {
signature,
form,
widget
widget,
};
/* eslint-enable no-underscore-dangle,no-param-reassign */
};
var _default = pdfkitAddPlaceholder;
exports.default = _default;
exports.default = _default;
@@ -1,20 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
var _abstract_reference = _interopRequireDefault(require("./pdfkit/abstract_reference"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
class PDFKitReferenceMock extends _abstract_reference.default {
constructor(index, additionalData = undefined) {
super();
this.index = index;
if (typeof additionalData !== 'undefined') {
if (typeof additionalData !== "undefined") {
Object.assign(this, additionalData);
}
}
@@ -22,8 +24,7 @@ class PDFKitReferenceMock extends _abstract_reference.default {
toString() {
return `${this.index} 0 R`;
}
}
var _default = PDFKitReferenceMock;
exports.default = _default;
exports.default = _default;
@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
@@ -9,7 +9,9 @@ var _findObject = _interopRequireDefault(require("./findObject"));
var _getIndexFromRef = _interopRequireDefault(require("./getIndexFromRef"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const createBufferPageWithAnnotation = (pdf, info, pagesRef, widget) => {
const pagesDictionary = (0, _findObject.default)(pdf, info.xref, pagesRef).toString(); // Extend page dictionary with newly created annotations
@@ -17,16 +19,16 @@ const createBufferPageWithAnnotation = (pdf, info, pagesRef, widget) => {
let annotsStart;
let annotsEnd;
let annots;
annotsStart = pagesDictionary.indexOf('/Annots');
annotsStart = pagesDictionary.indexOf("/Annots");
if (annotsStart > -1) {
annotsEnd = pagesDictionary.indexOf(']', annotsStart);
annotsEnd = pagesDictionary.indexOf("]", annotsStart);
annots = pagesDictionary.substr(annotsStart, annotsEnd + 1 - annotsStart);
annots = annots.substr(0, annots.length - 1); // remove the trailing ]
} else {
annotsStart = pagesDictionary.length;
annotsEnd = pagesDictionary.length;
annots = '/Annots [';
annots = "/Annots [";
}
const pagesDictionaryIndex = (0, _getIndexFromRef.default)(info.xref, pagesRef);
@@ -34,14 +36,19 @@ const createBufferPageWithAnnotation = (pdf, info, pagesRef, widget) => {
annots = `${annots} ${widgetValue}]`; // add the trailing ] back
const preAnnots = pagesDictionary.substr(0, annotsStart);
let postAnnots = '';
let postAnnots = "";
if (pagesDictionary.length > annotsEnd) {
postAnnots = pagesDictionary.substr(annotsEnd + 1);
}
return Buffer.concat([Buffer.from(`${pagesDictionaryIndex} 0 obj\n`), Buffer.from('<<\n'), Buffer.from(`${preAnnots + annots + postAnnots}\n`), Buffer.from('\n>>\nendobj\n')]);
return Buffer.concat([
Buffer.from(`${pagesDictionaryIndex} 0 obj\n`),
Buffer.from("<<\n"),
Buffer.from(`${preAnnots + annots + postAnnots}\n`),
Buffer.from("\n>>\nendobj\n"),
]);
};
var _default = createBufferPageWithAnnotation;
exports.default = _default;
exports.default = _default;
@@ -1,18 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
var _getIndexFromRef = _interopRequireDefault(require("./getIndexFromRef"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const createBufferRootWithAcroform = (pdf, info, form) => {
const rootIndex = (0, _getIndexFromRef.default)(info.xref, info.rootRef);
return Buffer.concat([Buffer.from(`${rootIndex} 0 obj\n`), Buffer.from('<<\n'), Buffer.from(`${info.root}\n`), Buffer.from(`/AcroForm ${form}`), Buffer.from('\n>>\nendobj\n')]);
return Buffer.concat([
Buffer.from(`${rootIndex} 0 obj\n`),
Buffer.from("<<\n"),
Buffer.from(`${info.root}\n`),
Buffer.from(`/AcroForm ${form}`),
Buffer.from("\n>>\nendobj\n"),
]);
};
var _default = createBufferRootWithAcroform;
exports.default = _default;
exports.default = _default;
@@ -1,21 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
const createBufferTrailer = (pdf, info, addedReferences) => {
let rows = [];
rows[0] = '0000000000 65535 f '; // info.xref.tableRows[0];
rows[0] = "0000000000 65535 f "; // info.xref.tableRows[0];
addedReferences.forEach((offset, index) => {
const paddedOffset = `0000000000${offset}`.slice(-10);
rows[index + 1] = `${index} 1\n${paddedOffset} 00000 n `;
});
rows = rows.filter(row => row !== undefined);
return Buffer.concat([Buffer.from('xref\n'), Buffer.from(`${info.xref.startingIndex} 1\n`), Buffer.from(rows.join('\n')), Buffer.from('\ntrailer\n'), Buffer.from('<<\n'), Buffer.from(`/Size ${info.xref.maxIndex + 1}\n`), Buffer.from(`/Root ${info.rootRef}\n`), Buffer.from(info.infoRef ? `/Info ${info.infoRef}\n` : ''), Buffer.from(`/Prev ${info.xRefPosition}\n`), Buffer.from('>>\n'), Buffer.from('startxref\n'), Buffer.from(`${pdf.length}\n`), Buffer.from('%%EOF')]);
rows = rows.filter((row) => row !== undefined);
return Buffer.concat([
Buffer.from("xref\n"),
Buffer.from(`${info.xref.startingIndex} 1\n`),
Buffer.from(rows.join("\n")),
Buffer.from("\ntrailer\n"),
Buffer.from("<<\n"),
Buffer.from(`/Size ${info.xref.maxIndex + 1}\n`),
Buffer.from(`/Root ${info.rootRef}\n`),
Buffer.from(info.infoRef ? `/Info ${info.infoRef}\n` : ""),
Buffer.from(`/Prev ${info.xRefPosition}\n`),
Buffer.from(">>\n"),
Buffer.from("startxref\n"),
Buffer.from(`${pdf.length}\n`),
Buffer.from("%%EOF"),
]);
};
var _default = createBufferTrailer;
exports.default = _default;
exports.default = _default;
@@ -1,13 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
value: true,
});
exports.default = void 0;
var _getIndexFromRef = _interopRequireDefault(require("./getIndexFromRef"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
/**
* @param {Buffer} pdf
@@ -18,12 +20,12 @@ const findObject = (pdf, refTable, ref) => {
const index = (0, _getIndexFromRef.default)(refTable, ref);
const offset = refTable.offsets.get(index);
let slice = pdf.slice(offset);
slice = slice.slice(0, slice.indexOf('endobj', 'utf8')); // FIXME: What if it is a stream?
slice = slice.slice(0, slice.indexOf("endobj", "utf8")); // FIXME: What if it is a stream?
slice = slice.slice(slice.indexOf('<<', 'utf8') + 2);
slice = slice.slice(0, slice.lastIndexOf('>>', 'utf8'));
slice = slice.slice(slice.indexOf("<<", "utf8") + 2);
slice = slice.slice(0, slice.lastIndexOf(">>", "utf8"));
return slice;
};
var _default = findObject;
exports.default = _default;
exports.default = _default;

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