wip: refresh design

This commit is contained in:
Mythie
2023-06-09 18:21:18 +10:00
parent 76b2fb5edd
commit 159bcade7b
432 changed files with 19640 additions and 29359 deletions

View File

@ -0,0 +1,42 @@
module.exports = {
extends: [
'next',
'turbo',
'prettier',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
plugins: ['prettier'],
env: {
node: true,
browser: true,
es6: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2022,
ecmaFeatures: {
jsx: true,
},
sourceType: 'module',
},
rules: {
'react/no-unescaped-entities': 'off',
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
// We never want to use `as` but are required to on occasion to handle
// shortcomings in third-party and generated types.
//
// To handle this we want this rule to catch usages and highlight them as
// warnings so we can write appropriate interfaces and guards later.
'@typescript-eslint/consistent-type-assertions': ['warn', { assertionStyle: 'never' }],
},
};

View File

@ -0,0 +1,17 @@
{
"name": "@documenso/eslint-config",
"version": "0.0.0",
"main": "./index.cjs",
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"eslint": "^8.40.0",
"eslint-config-next": "^13.4.1",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-config-turbo": "^1.9.3",
"typescript": "^5.0.4"
}
}

View File

@ -1,31 +0,0 @@
import { FieldType } from "@prisma/client";
export const createField = (
e: any,
page: number,
selectedRecipient: any,
type: FieldType = FieldType.SIGNATURE,
customText = ""
): any => {
var rect = e.target.getBoundingClientRect();
const fieldSize = { width: 192, height: 64 };
var newFieldX = e.clientX - rect.left - fieldSize.width / 2; //x position within the element.
var newFieldY = e.clientY - rect.top - fieldSize.height / 2; //y position within the element.
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;
const signatureField = {
id: -1,
page: page,
type: type,
positionX: newFieldX.toFixed(0),
positionY: newFieldY.toFixed(0),
Recipient: selectedRecipient,
customText: customText,
};
return signatureField;
};

View File

@ -1 +0,0 @@
export { createField } from "./createField";

View File

@ -1,40 +0,0 @@
The Documenso Commercial License (the “Commercial License”)
Copyright (c) 2023 Documenso, Inc
With regard to the Documenso Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, an agreement governing
the use of the Software, as mutually agreed by you and Documenso, Inc ("Documenso"),
and otherwise have a valid Documenso Enterprise Edition subscription ("Commercial Subscription").
Subject to the foregoing sentence, you are free to modify this Software and publish patches to the Software.
You agree that Documenso and/or its licensors (as applicable) retain all right, title and interest in
and to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid Commercial Subscription for the correct number of hosts.
Notwithstanding the foregoing, you may copy and modify the Software for development
and testing purposes, without requiring a subscription. You agree that Documenso and/or
its licensors (as applicable) retain all right, title and interest in and to all such
modifications. You are not granted any other rights beyond what is expressly stated herein.
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.
This Commercial License applies only to the part of this Software that is not distributed under
the AGPLv3 license. Any part of this Software distributed under the MIT license or which
is served client-side as an image, font, cascading stylesheet (CSS), file which produces
or is compiled, arranged, augmented, or combined into client-side JavaScript, in whole or
in part, is copyrighted under the AGPLv3 license. The full text of this Commercial License shall
be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
For all third party components incorporated into the Documenso Software, those
components are licensed under the original license provided by the owner of the
applicable component.

View File

@ -1,15 +0,0 @@
<div align="center"style="padding: 12px">
<a href="https://github.com/documenso/documenso.com">
<img width="250px" src="https://user-images.githubusercontent.com/1309312/224986248-5b8a5cdc-2dc1-46b9-a354-985bb6808ee0.png" alt="Documenso Logo">
</a>
<a href="https://dub.sh/documenso-enterprise">Contact Us</a>
</div>
# Enterprise Edition
Welcome to the Enterprise Edition ("/ee") of Documenso.com.
The [/ee](https://github.com/documenso/documenso/tree/main/packages/features/ee) subfolder is the home of all the **Enterprise Edition** features from our [hosted](https://documenso.com/pricing) plan. To use this code in production you need and valid Enterprise License.
> IMPORTANT: This subfolder is licensed differently than the rest of our [main repo](https://github.com/documenso/documenso). [Contact us](https://dub.sh/documenso-enterprise) to learn more.

View File

@ -1,2 +0,0 @@
export { uploadDocument } from "./uploadDocument";
export { updateUser } from "./updateUser";

View File

@ -1,8 +0,0 @@
{
"name": "@documenso/features",
"description": "Main features of documenso in one neat package.",
"version": "0.0.0",
"private": true,
"main": "index.ts",
"dependencies": {}
}

View File

@ -1,19 +0,0 @@
import toast from "react-hot-toast";
export const updateUser = async (user: any) => {
if (!user) return;
toast.promise(
fetch("/api/users", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(user),
}),
{
loading: "Saving Changes...",
success: `Saved!`,
error: "Changes could not save user :/",
}
);
};

View File

@ -1,43 +0,0 @@
import { ChangeEvent } from "react";
import router from "next/router";
import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants";
import toast from "react-hot-toast";
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;
if (!fileName.endsWith(".pdf")) {
toast.error("Non-PDF documents are not supported yet.");
return;
}
body.append("document", document || "");
await toast.promise(
fetch("/api/documents", {
method: "POST",
body,
}).then((response: Response) => {
if (!response.ok) {
throw new Error("Could not upload document");
}
response.json().then((createdDocumentIdFromBody) => {
router.push(
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${createdDocumentIdFromBody}/recipients`
);
});
}),
{
loading: "Uploading document...",
success: `${fileName} uploaded successfully.`,
error: "Could not upload document :/",
}
).catch((_err) => {
// Do nothing
});
}
};

View File

@ -1,36 +0,0 @@
import toast from "react-hot-toast";
export const createOrUpdateField = async (
document: any,
field: any,
recipientToken: string = ""
): Promise<any> => {
try {
const created = await toast.promise(
fetch("/api/documents/" + document.id + "/fields?token=" + recipientToken, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(field),
}).then((res) => {
if (!res.ok) {
throw new Error(res.status.toString());
}
return res.json();
}),
{
loading: field?.id !== -1 ? "Saving..." : "Adding...",
success: field?.id !== -1 ? "Saved." : "Added.",
error: field?.id !== -1 ? "Could not save :/" : "Could not add :/",
},
{
id: "saving field",
style: {
minWidth: "200px",
},
}
);
return created;
} catch (error) {}
};

View File

@ -1,32 +0,0 @@
import toast from "react-hot-toast";
export const createOrUpdateRecipient = async (recipient: any): Promise<any> => {
try {
const created = await toast.promise(
fetch("/api/documents/" + recipient.documentId + "/recipients", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(recipient),
}).then((res) => {
if (!res.ok) {
throw new Error(res.status.toString());
}
return res.json();
}),
{
loading: "Saving...",
success: "Saved.",
error: "Could not save :/",
},
{
id: "saving",
style: {
minWidth: "200px",
},
}
);
return created;
} catch (error) {}
};

View File

@ -1,5 +0,0 @@
export const deleteDocument = (documentId: number): Promise<Response> => {
return fetch(`/api/documents/${documentId}`, {
method: "DELETE",
});
};

View File

@ -1,36 +0,0 @@
import toast from "react-hot-toast";
export const deleteField = async (field: any) => {
if (!field.id) {
return;
}
try {
const deleted = toast.promise(
fetch("/api/documents/" + 0 + "/fields/" + field.id, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(field),
}).then((res) => {
if (!res.ok) {
throw new Error(res.status.toString());
}
return res;
}),
{
loading: "Deleting...",
success: "Deleted.",
error: "Could not delete :/",
},
{
id: "delete",
style: {
minWidth: "200px",
},
}
);
return deleted;
} catch (error) {}
};

View File

@ -1,28 +0,0 @@
import toast from "react-hot-toast";
export const deleteRecipient = (recipient: any) => {
if (!recipient.id) {
return;
}
return toast.promise(
fetch("/api/documents/" + recipient.documentId + "/recipients/" + recipient.id, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(recipient),
}),
{
loading: "Deleting...",
success: "Deleted.",
error: "Could not delete :/",
},
{
id: "delete",
style: {
minWidth: "200px",
},
}
);
};

View File

@ -1,7 +0,0 @@
export const getDocuments = (): Promise<Response> => {
return fetch("/api/documents", {
headers: {
"Content-Type": "application/json",
},
});
};

View File

@ -1,3 +0,0 @@
export const getUser = (): Promise<Response> => {
return fetch("/api/users/me");
};

View File

@ -1,10 +0,0 @@
export { createOrUpdateField } from "./createOrUpdateField";
export { deleteField } from "./deleteField";
export { signDocument } from "./signDocument";
export { getUser } from "./getUser";
export { signup } from "./signup";
export { getDocuments } from "./getDocuments";
export { deleteDocument } from "./deleteDocument";
export { deleteRecipient } from "./deleteRecipient";
export { createOrUpdateRecipient } from "./createOrUpdateRecipient";
export { sendSigningRequests } from "./sendSigningRequests";

View File

@ -1,29 +0,0 @@
import toast from "react-hot-toast";
export const sendSigningRequests = async (document: any, resendTo: number[] = []) => {
if (!document || !document.id) return;
try {
const sent = await toast.promise(
fetch(`/api/documents/${document.id}/send`, {
body: JSON.stringify({ resendTo: resendTo }),
headers: { "Content-Type": "application/json" },
method: "POST",
})
.then((res: any) => {
if (!res.ok) {
throw new Error(res.status.toString());
}
})
.finally(() => {
location.reload();
}),
{
loading: "Sending...",
success: `Sent!`,
error: "Could not send :/",
}
);
} catch (err) {
console.log(err);
}
};

View File

@ -1,21 +0,0 @@
import { useRouter } from "next/router";
import toast from "react-hot-toast";
export const signDocument = (document: any, signatures: any[], token: string): Promise<any> => {
const body = { documentId: document.id, signatures };
return toast.promise(
fetch(`/api/documents/${document.id}/sign?token=${token}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}),
{
loading: "Signing...",
success: `"${document.title}" signed successfully.`,
error: "Could not sign :/",
}
);
};

View File

@ -1,12 +0,0 @@
export const signup = (source: any, data: any): Promise<Response> => {
return fetch("/api/auth/signup", {
body: JSON.stringify({
source: source,
...data,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
};

View File

@ -1,89 +0,0 @@
import type { NextApiRequest } from "next";
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);
return hashedPassword;
}
export async function verifyPassword(password: string, hashedPassword: string) {
const isValid = await compare(password, hashedPassword);
return isValid;
}
export function validPassword(password: string) {
if (password.length < 7) return false;
if (!/[A-Z]/.test(password) || !/[a-z]/.test(password)) return false;
if (!/\d+/.test(password)) return false;
return true;
}
export async function getSession(options: GetSessionParams): Promise<Session | null> {
const session = await getSessionInner(options);
// that these are equal are ensured in `[...nextauth]`'s callback
return session as Session | null;
}
export function isPasswordValid(password: string): boolean;
export function isPasswordValid(
password: string,
breakdown: boolean,
strict?: boolean
): { caplow: boolean; num: boolean; min: boolean; admin_min: 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
min = false, // Eight characters, or fifteen in strict mode.
admin_min = false;
if (password.length > 7 && (!strict || password.length > 14)) min = true;
if (strict && password.length > 14) admin_min = true;
for (let i = 0; i < password.length; i++) {
if (!isNaN(parseInt(password[i]))) num = true;
else {
if (password[i] === password[i].toUpperCase()) cap = true;
if (password[i] === password[i].toLowerCase()) low = 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.
if (strict) errors = { ...errors, admin_min };
return errors;
}
type CtxOrReq =
| { req: NextApiRequest; ctx?: never }
| { ctx: { req: NextApiRequest }; req?: never };
export const ensureSession = async (ctxOrReq: CtxOrReq) => {
const session = await getSession(ctxOrReq);
if (!session?.user) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
return session;
};
export enum ErrorCode {
UserNotFound = "user-not-found",
IncorrectPassword = "incorrect-password",
UserMissingPassword = "missing-password",
TwoFactorDisabled = "two-factor-disabled",
TwoFactorAlreadyEnabled = "two-factor-already-enabled",
TwoFactorSetupRequired = "two-factor-setup-required",
SecondFactorRequired = "second-factor-required",
IncorrectTwoFactorCode = "incorrect-two-factor-code",
InternalServerError = "internal-server-error",
NewPasswordMatchesOld = "new-password-matches-old",
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
RateLimitExceeded = "rate-limit-exceeded",
SocialIdentityProviderRequired = "social-identity-provider-required",
}

View File

@ -1,3 +0,0 @@
export default function classNames(...classes: unknown[]) {
return classes.filter(Boolean).join(" ");
}

View File

@ -0,0 +1,21 @@
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
export const useUpdateSearchParams = () => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
return (params: Record<string, string | number | boolean | null | undefined>) => {
const nextSearchParams = new URLSearchParams(searchParams?.toString() ?? '');
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null) {
nextSearchParams.delete(key);
} else {
nextSearchParams.set(key, String(value));
}
});
router.push(`${pathname}?${nextSearchParams.toString()}`);
};
};

View File

@ -1,40 +0,0 @@
// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
export class coloredConsole {
public static setupColoredConsole(): void {
let infoLog = console.info;
let logLog = console.log;
let errorLog = console.error;
let warnLog = console.warn;
let colors = {
Reset: "\x1b[0m",
Red: "\x1b[31m",
Green: "\x1b[32m",
Yellow: "\x1b[33m",
};
console.info = function (args: any) {
let copyArgs = Array.prototype.slice.call(arguments);
copyArgs.unshift(colors.Green);
copyArgs.push(colors.Reset);
infoLog.apply(null, copyArgs);
};
console.warn = function (args: any) {
let copyArgs = Array.prototype.slice.call(arguments);
copyArgs.unshift(colors.Yellow);
copyArgs.push(colors.Reset);
warnLog.apply(null, copyArgs);
};
console.error = function (args: any) {
let copyArgs = Array.prototype.slice.call(arguments);
copyArgs.unshift(colors.Red);
copyArgs.push(colors.Reset);
errorLog.apply(null, copyArgs);
};
}
}
coloredConsole.setupColoredConsole();

View File

@ -1,4 +0,0 @@
export const NEXT_PUBLIC_WEBAPP_URL =
process.env.IS_PULL_REQUEST === "true"
? process.env.RENDER_EXTERNAL_URL
: process.env.NEXT_PUBLIC_WEBAPP_URL;

View File

@ -0,0 +1 @@
export const SALT_ROUNDS = 12;

View File

@ -0,0 +1,5 @@
/* eslint-disable turbo/no-undeclared-env-vars */
export const IS_SUBSCRIPTIONS_ENABLED = process.env.NEXT_PUBLIC_SUBSCRIPTIONS_ENABLED === 'true';
export const isSubscriptionsEnabled = () =>
process.env.NEXT_PUBLIC_SUBSCRIPTIONS_ENABLED === 'true';

View File

@ -1 +0,0 @@
export const isENVProd = process.env.NODE_ENV === "production";

View File

@ -0,0 +1,5 @@
export class UserExistsError extends Error {
constructor() {
super('User already exists');
}
}

View File

@ -1,24 +0,0 @@
// It ensures that redirection URL safe where it is accepted through a query params or other means where user can change it.
export const getSafeRedirectUrl = (url = "") => {
if (!url) {
return null;
}
//It is important that this fn is given absolute URL because urls that don't start with HTTP can still deceive browser into redirecting to another domain
if (url.search(/^https?:\/\//) === -1) {
throw new Error("Pass an absolute URL");
}
const urlParsed = new URL(url);
// Avoid open redirection security vulnerability
if (
!["CONSOLE_URL", "WEBAPP_URL", "WEBSITE_URL"].some(
(u) => new URL(u).origin === urlParsed.origin
)
) {
url = `${"WEBAPP_URL"}/`;
}
return url;
};

View File

@ -1 +0,0 @@
export * from './strings';

View File

@ -1,13 +0,0 @@
/**
* Truncates a title to a given max length substituting the middle with an ellipsis.
*/
export const truncate = (str: string, maxLength: number = 20) => {
if (str.length <= maxLength) {
return str;
}
const startLength = Math.ceil((maxLength - 3) / 2);
const endLength = Math.floor((maxLength - 3) / 2);
return `${str.slice(0, startLength)}...${str.slice(-endLength)}`;
};

View File

@ -1,5 +1 @@
export { coloredConsole } from "./coloredConsole";
export { default as classNames } from "./classNames";
export { NEXT_PUBLIC_WEBAPP_URL } from "./constants";
export { localStorage } from "./webstorage";
export { isENVProd } from "./env";
export {};

View File

@ -1,36 +0,0 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
export const baseEmailTemplate = (message: string, content: string) => {
const html = `
<div style="background-color: #eaeaea; padding: 2%;">
<div style="text-align:center; margin: auto; font-size: 14px; color: #353434; max-width: 500px; border-radius: 0.375rem; background: white; padding: 50px">
<img src="${NEXT_PUBLIC_WEBAPP_URL}/logo_h.png" alt="Documenso Logo" style="width: 180px; display: block; margin: auto; margin-bottom: 14px;">
${message}
${content}
</div>
`;
const footer = `
<div style="text-align: left; line-height: 18px; color: #666666; margin: 24px">
<div>
<b>Do not forward.</b>
<br>
This email gives access to a secure document. Keep it secret and do not forward this email.
</div>
<div style="margin-top: 12px">
<b>Need help?</b>
<br>
Contact us at <a href="mailto:hi@documenso.com">hi@documenso.com</a>
</div>
<hr size="1" style="height: 1px; border: none; color: #D8D8D8; background-color: #D8D8D8">
<div style="text-align: center">
<small>Easy and beautiful document signing by Documenso.</small>
</div>
</div>
</div>
`;
return html + footer;
};
export default baseEmailTemplate;

View File

@ -1,8 +0,0 @@
export { signingRequestTemplate } from "./signingRequestTemplate";
export { signingCompleteTemplate } from "./signingCompleteTemplate";
export { sendSigningRequest as sendSigningRequest } from "./sendSigningRequest";
export { sendSigningDoneMail } from "./sendSigningDoneMail";
export { resetPasswordTemplate } from "./resetPasswordTemplate";
export { sendResetPassword } from "./sendResetPassword";
export { resetPasswordSuccessTemplate } from "./resetPasswordSuccessTemplate";
export { sendResetPasswordSuccessMail } from "./sendResetPasswordSuccessMail";

View File

@ -1,47 +0,0 @@
import nodemailer from "nodemailer";
import nodemailerSendgrid from "nodemailer-sendgrid";
export const sendMail = async (
to: string,
subject: string,
body: string,
attachments: {
filename: string;
content: string | Buffer;
}[] = []
) => {
let transport;
if (process.env.SENDGRID_API_KEY)
transport = nodemailer.createTransport(
nodemailerSendgrid({
apiKey: process.env.SENDGRID_API_KEY || "",
})
);
if (process.env.SMTP_MAIL_HOST)
transport = nodemailer.createTransport({
host: process.env.SMTP_MAIL_HOST || "",
port: Number(process.env.SMTP_MAIL_PORT) || 587,
auth: {
user: process.env.SMTP_MAIL_USER || "",
pass: process.env.SMTP_MAIL_PASSWORD || "",
},
});
if (!transport)
throw new Error(
"No valid transport for NodeMailer found. Probably Sendgrid API Key nor SMTP Mail host was set."
);
await transport
.sendMail({
from: process.env.MAIL_FROM,
to: to,
subject: subject,
html: body,
attachments: attachments,
})
.catch((err) => {
throw err;
});
};

View File

@ -1,18 +0,0 @@
import { signingCompleteTemplate } from "@documenso/lib/mail";
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
import { sendMail } from "./sendMail";
import { Document as PrismaDocument } from "@prisma/client";
export const sendSigningDoneMail = async (document: PrismaDocument, user: any) => {
await sendMail(
user.email,
`Completed: "${document.title}"`,
signingCompleteTemplate(`All recipients have signed "${document.title}".`),
[
{
filename: document.title,
content: Buffer.from(await addDigitalSignature(document.document), "base64"),
},
]
);
};

View File

@ -1,47 +0,0 @@
import { signingRequestTemplate } from "@documenso/lib/mail";
import prisma from "@documenso/prisma";
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { sendMail } from "./sendMail";
import { DocumentStatus, ReadStatus, SendStatus } from "@prisma/client";
export const sendSigningRequest = async (recipient: any, document: any, user: any) => {
const signingRequestMessage = user.name
? `${user.name} (${user.email}) has sent you a document to sign. `
: `${user.email} has sent you a document to sign. `;
await sendMail(
recipient.email,
`Please sign ${document.title}`,
signingRequestTemplate(
signingRequestMessage,
document,
recipient,
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${document.id}/sign?token=${recipient.token}`,
`Sign Document`,
user
)
).catch((err) => {
throw err;
});
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 60);
await prisma.recipient.update({
where: {
id: recipient.id,
},
data: {
sendStatus: SendStatus.SENT,
readStatus: ReadStatus.NOT_OPENED,
expired: expiryDate,
},
});
await prisma.document.update({
where: {
id: document.id,
},
data: { status: DocumentStatus.PENDING },
});
};

View File

@ -1,27 +0,0 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { baseEmailTemplate } from "./baseTemplate";
export const signingCompleteTemplate = (message: string) => {
const customContent = `
<div style="
width: 100px;
height: 100px;
margin: auto;
padding-top: 14px;
">
<img src="${NEXT_PUBLIC_WEBAPP_URL}/images/signed_100.png" alt="Documenso Logo" style="width: 100px; display: block;">
</div>
<p style="margin-top: 14px;">
A copy of the signed document has been attached to this email.
</p>
<p style="margin-top: 14px;">
<small>Like Documenso? <a href="https://documenso.com">Hosted Documenso is here!</a>.</small>
</p>`;
const html = baseEmailTemplate(message, customContent);
return html;
};
export default signingCompleteTemplate;

View File

@ -1,32 +0,0 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { baseEmailTemplate } from "./baseTemplate";
import { Document as PrismaDocument } from "@prisma/client";
export const signingRequestTemplate = (
message: string,
document: any,
recipient: any,
ctaLink: string,
ctaLabel: string,
user: any
) => {
const customContent = `
<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>
<hr size="1" style="height:1px;border:none;color:#e0e0e0;background-color:#e0e0e0">
Click the button to view "${document.title}".<br>
<small>If you have questions about this document, you should ask ${user.name ?? user.email}.</small>
<hr size="1" style="height:1px;border:none;color:#e0e0e0;background-color:#e0e0e0">
<p style="margin-top: 14px;">
<small>Want to send you own signing links? <a href="https://documenso.com">Hosted Documenso is here!</a>.</small>
</p>`;
const html = baseEmailTemplate(message, customContent);
return html;
};
export default signingRequestTemplate;

View File

@ -0,0 +1,59 @@
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import { compare } from 'bcrypt';
import { AuthOptions, User } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { prisma } from '@documenso/prisma';
import { getUserByEmail } from '../server-only/user/get-user-by-email';
export const NEXT_AUTH_OPTIONS: AuthOptions = {
adapter: PrismaAdapter(prisma),
secret: process.env.NEXTAUTH_SECRET ?? 'secret',
session: {
strategy: 'jwt',
},
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials, _req) => {
if (!credentials) {
return null;
}
const { email, password } = credentials;
const user = await getUserByEmail({ email }).catch(() => null);
if (!user || !user.password) {
console.log('no user');
return null;
}
const isPasswordsSame = compare(password, user.password);
if (!isPasswordsSame) {
return null;
}
return {
id: String(user.id) as any,
email: user.email,
name: user.name,
image: '',
} satisfies User;
},
}),
],
// callbacks: {
// jwt: async ({ token, user: _user }) => {
// return {
// ...token,
// };
// },
// },
};

View File

@ -0,0 +1,54 @@
import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from 'next';
import { getServerSession as getNextAuthServerSession } from 'next-auth';
import { prisma } from '@documenso/prisma';
import { NEXT_AUTH_OPTIONS } from './auth-options';
export interface GetServerSessionOptions {
req: NextApiRequest | GetServerSidePropsContext['req'];
res: NextApiResponse | GetServerSidePropsContext['res'];
}
export const getServerSession = async ({ req, res }: GetServerSessionOptions) => {
const session = await getNextAuthServerSession(req, res, NEXT_AUTH_OPTIONS);
if (!session || !session.user?.email) {
return null;
}
const user = await prisma.user.findFirstOrThrow({
where: {
email: session.user.email,
},
});
return user;
};
export const getServerComponentSession = async () => {
const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS);
if (!session || !session.user?.email) {
return null;
}
const user = await prisma.user.findFirstOrThrow({
where: {
email: session.user.email,
},
});
return user;
};
export const getRequiredServerComponentSession = async () => {
const session = await getServerComponentSession();
if (!session) {
throw new Error('No session found');
}
return session;
};

View File

@ -1,13 +1,29 @@
{
"name": "@documenso/lib",
"version": "0.0.0",
"private": true,
"main": "index.ts",
"version": "1.0.0",
"main": "./index.ts",
"types": "./index.ts",
"license": "MIT",
"files": [
"client-only/",
"server-only/",
"universal/",
"next-auth/"
],
"scripts": {
},
"dependencies": {
"@documenso/prisma": "*",
"@prisma/client": "^4.8.1",
"bcryptjs": "^2.4.3",
"micro": "^10.0.1",
"stripe": "^12.4.0"
"@pdf-lib/fontkit": "^1.1.1",
"@next-auth/prisma-adapter": "^1.0.6",
"@upstash/redis": "^1.20.6",
"bcrypt": "^5.1.0",
"pdf-lib": "^1.17.1",
"next": "13.4.1",
"next-auth": "^4.22.1",
"stripe": "^12.7.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0"
}
}
}

View File

@ -1,24 +0,0 @@
declare namespace NodeJS {
export interface ProcessEnv {
DATABASE_URL: string;
NEXT_PUBLIC_WEBAPP_URL: string;
NEXTAUTH_SECRET: string;
NEXTAUTH_URL: string;
SENDGRID_API_KEY?: string;
SMTP_MAIL_HOST?: string;
SMTP_MAIL_PORT?: string;
SMTP_MAIL_USER?: string;
SMTP_MAIL_PASSWORD?: string;
MAIL_FROM: string;
STRIPE_API_KEY?: string;
STRIPE_WEBHOOK_SECRET?: string;
STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID?: string;
STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID?: string;
NEXT_PUBLIC_ALLOW_SIGNUP?: string;
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS?: string;
}
}

View File

@ -1,31 +0,0 @@
import { getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { Document as PrismaDocument } from "@prisma/client";
export const getDocument = async (
documentId: number,
req: any,
res: any
): Promise<PrismaDocument> => {
const user = await getUserFromToken(req, res);
if (!user) return Promise.reject("Invalid user or token.");
if (!documentId) Promise.reject("No documentId");
if (!req || !res) Promise.reject("No res or req");
const document: PrismaDocument = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
userId: user.id,
},
include: {
Recipient: {
orderBy: {
id: "asc",
},
},
Field: { include: { Recipient: true, Signature: true } },
},
});
return document;
};

View File

@ -1,21 +0,0 @@
import { getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
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.");
const documents = await prisma.document.findMany({
where: {
userId: user.id,
},
include: {
Recipient: true,
},
orderBy: {
created: "desc",
},
});
return documents.map((e) => ({ ...e, document: "" }));
};

View File

@ -1,2 +0,0 @@
export { getDocumentsForUserFromToken } from "./getDocumentsForUserFromToken";
export { getDocument } from "./getDocument";

View File

@ -0,0 +1,10 @@
import { hashSync as bcryptHashSync } from 'bcrypt';
import { SALT_ROUNDS } from '../../constants/auth';
/**
* @deprecated Use the methods built into `bcrypt` instead
*/
export const hashSync = (password: string) => {
return bcryptHashSync(password, SALT_ROUNDS);
};

View File

@ -0,0 +1,66 @@
import { prisma } from '@documenso/prisma';
import { Document, DocumentStatus, Prisma } from '@documenso/prisma/client';
import { FindResultSet } from '../../types/find-result-set';
export interface FindDocumentsOptions {
userId: number;
term?: string;
status?: DocumentStatus;
page?: number;
perPage?: number;
orderBy?: {
column: keyof Omit<Document, 'document'>;
direction: 'asc' | 'desc';
};
}
export const findDocuments = async ({
userId,
term,
status,
page = 1,
perPage = 10,
orderBy,
}: FindDocumentsOptions): Promise<FindResultSet<Document>> => {
const orderByColumn = orderBy?.column ?? 'created';
const orderByDirection = orderBy?.direction ?? 'desc';
const filters: Prisma.DocumentWhereInput = {
status,
userId,
};
if (term) {
filters.title = {
contains: term,
mode: 'insensitive',
};
}
const [data, count] = await Promise.all([
prisma.document.findMany({
where: {
...filters,
},
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
[orderByColumn]: orderByDirection,
},
}),
prisma.document.count({
where: {
...filters,
},
}),
]);
return {
data,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
};
};

View File

@ -0,0 +1,15 @@
import { prisma } from '@documenso/prisma';
export interface GetDocumentByIdOptions {
id: number;
userId: number;
}
export const getDocumentById = async ({ id, userId }: GetDocumentByIdOptions) => {
return await prisma.document.findFirstOrThrow({
where: {
id,
userId,
},
});
};

View File

@ -0,0 +1,30 @@
import { prisma } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client';
export type GetStatsInput = {
userId: number;
};
export const getStats = async ({ userId }: GetStatsInput) => {
const result = await prisma.document.groupBy({
by: ['status'],
_count: {
_all: true,
},
where: {
userId,
},
});
const stats: Record<DocumentStatus, number> = {
[DocumentStatus.DRAFT]: 0,
[DocumentStatus.PENDING]: 0,
[DocumentStatus.COMPLETED]: 0,
};
result.forEach((stat) => {
stats[stat.status] = stat._count._all;
});
return stats;
};

View File

@ -0,0 +1,11 @@
import { headers } from 'next/headers';
export const getLocale = () => {
const headerItems = headers();
const locales = headerItems.get('accept-language') ?? 'en-US';
const [locale] = locales.split(',');
return locale;
};

View File

@ -1,11 +1,11 @@
import { PDFDocument } from "pdf-lib";
import { PDFDocument } from 'pdf-lib';
export async function insertImageInPDF(
pdfAsBase64: string,
image: string | Uint8Array | ArrayBuffer,
positionX: number,
positionY: number,
page: number = 0
page = 0,
): Promise<string> {
const existingPdfBytes = pdfAsBase64;
const pdfDoc = await PDFDocument.load(existingPdfBytes);
@ -22,5 +22,5 @@ export async function insertImageInPDF(
});
const pdfAsUint8Array = await pdfDoc.save();
return Buffer.from(pdfAsUint8Array).toString("base64");
return Buffer.from(pdfAsUint8Array).toString('base64');
}

View File

@ -1,16 +1,16 @@
import fontkit from "@pdf-lib/fontkit";
import * as fs from "fs";
import { PDFDocument, StandardFonts, rgb } 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,
text: string,
positionX: number,
positionY: number,
page: number = 0,
useHandwritingFont = true
page = 0,
useHandwritingFont = true,
): Promise<string> {
const fontBytes = fs.readFileSync("public/fonts/Qwigley-Regular.ttf");
const fontBytes = fs.readFileSync('./public/fonts/caveat.ttf');
const pdfDoc = await PDFDocument.load(pdfAsBase64);
@ -24,7 +24,7 @@ export async function insertTextInPDF(
const textSize = useHandwritingFont ? 50 : 15;
const textWidth = font.widthOfTextAtSize(text, textSize);
const textHeight = font.heightAtSize(textSize);
const fieldSize = { width: 192, height: 64 };
const fieldSize = { width: 250, height: 64 };
// 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
@ -45,5 +45,6 @@ export async function insertTextInPDF(
});
const pdfAsUint8Array = await pdfDoc.save();
return Buffer.from(pdfAsUint8Array).toString("base64");
return Buffer.from(pdfAsUint8Array).toString('base64');
}

View File

@ -0,0 +1,8 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { Redis } from '@upstash/redis';
// !: We're null coalescing here because we don't want local builds to fail.
export const redis = new Redis({
url: process.env.NEXT_PRIVATE_REDIS_URL ?? '',
token: process.env.NEXT_PRIVATE_REDIS_TOKEN ?? '',
});

View File

@ -0,0 +1,9 @@
import Stripe from 'stripe';
// eslint-disable-next-line turbo/no-undeclared-env-vars
export const stripe = new Stripe(process.env.NEXT_PRIVATE_STRIPE_API_KEY!, {
apiVersion: '2022-11-15',
typescript: true,
});
export { Stripe };

View File

@ -0,0 +1,35 @@
import { hash } from 'bcrypt';
import { prisma } from '@documenso/prisma';
import { IdentityProvider } from '@documenso/prisma/client';
import { SALT_ROUNDS } from '../../constants/auth';
export interface CreateUserOptions {
name: string;
email: string;
password: string;
}
export const createUser = async ({ name, email, password }: CreateUserOptions) => {
const hashedPassword = await hash(password, SALT_ROUNDS);
const userExists = await prisma.user.findFirst({
where: {
email: email.toLowerCase(),
},
});
if (userExists) {
throw new Error('User already exists');
}
return await prisma.user.create({
data: {
name,
email: email.toLowerCase(),
password: hashedPassword,
identityProvider: IdentityProvider.DOCUMENSO,
},
});
};

View File

@ -0,0 +1,13 @@
import { prisma } from '@documenso/prisma';
export interface GetUserByEmailOptions {
email: string;
}
export const getUserByEmail = async ({ email }: GetUserByEmailOptions) => {
return await prisma.user.findFirstOrThrow({
where: {
email: email.toLowerCase(),
},
});
};

View File

@ -0,0 +1,13 @@
import { prisma } from '@documenso/prisma';
export interface GetUserByIdOptions {
id: number;
}
export const getUserById = async ({ id }: GetUserByIdOptions) => {
return await prisma.user.findFirstOrThrow({
where: {
id,
},
});
};

View File

@ -0,0 +1,32 @@
import { hash } from 'bcrypt';
import { prisma } from '@documenso/prisma';
import { SALT_ROUNDS } from '../../constants/auth';
export type UpdatePasswordOptions = {
userId: number;
password: string;
};
export const updatePassword = async ({ userId, password }: UpdatePasswordOptions) => {
// Existence check
await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const hashedPassword = await hash(password, SALT_ROUNDS);
const updatedUser = await prisma.user.update({
where: {
id: userId,
},
data: {
password: hashedPassword,
},
});
return updatedUser;
};

View File

@ -0,0 +1,33 @@
import { prisma } from '@documenso/prisma';
export type UpdateProfileOptions = {
userId: number;
name: string;
signature: string;
};
export const updateProfile = async ({
userId,
name,
// TODO: Actually use signature
signature: _signature,
}: UpdateProfileOptions) => {
// Existence check
await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const updatedUser = await prisma.user.update({
where: {
id: userId,
},
data: {
name,
// signature,
},
});
return updatedUser;
};

View File

@ -1,27 +0,0 @@
import type { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
type Handlers = {
[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(",")})`,
});
}
try {
await handler(req, res);
return;
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Something went wrong" });
}
};

View File

@ -1,19 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerErrorFromUnknown } from "@documenso/lib/server";
type Handle<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>;
/** Allows us to get type inference from API handler responses */
export function defaultResponder<T>(f: Handle<T>) {
return async (req: NextApiRequest, res: NextApiResponse) => {
try {
const result = await f(req, res);
if (result) res.json(result);
} catch (err) {
console.error(err);
const error = getServerErrorFromUnknown(err);
res.statusCode = error.statusCode;
res.json({ message: error.message });
}
};
}

View File

@ -1,39 +0,0 @@
import { HttpError } from "@documenso/lib/server";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
export function getServerErrorFromUnknown(cause: unknown): HttpError {
// Error was manually thrown and does not need to be parsed.
if (cause instanceof HttpError) {
return cause;
}
if (cause instanceof SyntaxError) {
return new HttpError({
statusCode: 500,
message: "Unexpected error, please reach out for our customer support.",
});
}
if (cause instanceof PrismaClientKnownRequestError) {
return new HttpError({ statusCode: 400, message: cause.message, cause });
}
if (cause instanceof PrismaClientKnownRequestError) {
return new HttpError({ statusCode: 404, message: cause.message, cause });
}
if (cause instanceof Error) {
return new HttpError({ statusCode: 500, message: cause.message, cause });
}
if (typeof cause === "string") {
// @ts-expect-error https://github.com/tc39/proposal-error-cause
return new Error(cause, { cause });
}
// Catch-All if none of the above triggered and something (even more) unexpected happened
return new HttpError({
statusCode: 500,
message: `Unhandled error of type '${typeof cause}'. Please reach out for our customer support.`,
});
}

View File

@ -1,27 +0,0 @@
import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next";
import { NextRequest } from "next/server";
import prisma from "@documenso/prisma";
import { User as PrismaUser } from "@prisma/client";
import { getToken } from "next-auth/jwt";
export async function getUserFromToken(
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
res?: NextApiResponse // TODO: Remove this optional parameter
): Promise<PrismaUser | null> {
const token = await getToken({ req });
const tokenEmail = token?.email?.toString();
if (!token || !tokenEmail) {
return null;
}
const user = await prisma.user.findFirst({
where: { email: tokenEmail },
});
if (!user) {
return null;
}
return user;
}

View File

@ -1,39 +0,0 @@
export class HttpError<TCode extends number = number> extends Error {
public readonly cause?: Error;
public readonly statusCode: TCode;
public readonly message: string;
public readonly url: string | undefined;
public readonly method: string | undefined;
constructor(opts: {
url?: string;
method?: string;
message?: string;
statusCode: TCode;
cause?: Error;
}) {
super(opts.message ?? `HTTP Error ${opts.statusCode} `);
Object.setPrototypeOf(this, HttpError.prototype);
this.name = HttpError.prototype.constructor.name;
this.cause = opts.cause;
this.statusCode = opts.statusCode;
this.url = opts.url;
this.method = opts.method;
this.message = opts.message ?? `HTTP Error ${opts.statusCode}`;
if (opts.cause instanceof Error && opts.cause.stack) {
this.stack = opts.cause.stack;
}
}
public static fromRequest(request: Request, response: Response) {
return new HttpError({
message: response.statusText,
url: response.url,
method: request.method,
statusCode: response.status,
});
}
}

View File

@ -1,5 +0,0 @@
export { defaultHandler } from "./defaultHandler";
export { defaultResponder } from "./defaultResponder";
export { HttpError } from "./http-error";
export { getServerErrorFromUnknown } from "./getServerErrorFromUnknown";
export { getUserFromToken } from "./getUserFromToken";

View File

@ -1,7 +0,0 @@
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_API_KEY!, {
apiVersion: "2022-11-15",
typescript: true,
});

View File

@ -1,15 +0,0 @@
export const STRIPE_PLANS = [
{
name: "Community Plan",
prices: {
monthly: {
price: 30,
priceId: process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID ?? "",
},
yearly: {
price: 300,
priceId: process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID ?? "",
},
},
},
];

View File

@ -1,23 +0,0 @@
import { CheckoutSessionRequest, CheckoutSessionResponse } from "../handlers/checkout-session"
export type FetchCheckoutSessionOptions = CheckoutSessionRequest['body']
export const fetchCheckoutSession = async ({
id,
priceId
}: FetchCheckoutSessionOptions) => {
const response = await fetch('/api/stripe/checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id,
priceId
})
});
const json: CheckoutSessionResponse = await response.json();
return json;
}

View File

@ -1,14 +0,0 @@
import { GetSubscriptionResponse } from "../handlers/get-subscription";
export const fetchSubscription = async () => {
const response = await fetch("/api/stripe/subscription", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const json: GetSubscriptionResponse = await response.json();
return json;
};

View File

@ -1,19 +0,0 @@
import { PortalSessionRequest, PortalSessionResponse } from "../handlers/portal-session";
export type FetchPortalSessionOptions = PortalSessionRequest["body"];
export const fetchPortalSession = async ({ id }: FetchPortalSessionOptions) => {
const response = await fetch("/api/stripe/portal-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id,
}),
});
const json: PortalSessionResponse = await response.json();
return json;
};

View File

@ -1,35 +0,0 @@
import { GetServerSideProps, GetServerSidePropsContext, NextApiRequest } from "next";
import { SubscriptionStatus } from "@prisma/client";
import { getToken } from "next-auth/jwt";
export const isSubscriptionsEnabled = () => {
return process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true";
};
export const isSubscribedServer = async (
req: NextApiRequest | GetServerSidePropsContext["req"]
) => {
const { default: prisma } = await import("@documenso/prisma");
if (!isSubscriptionsEnabled()) {
return true;
}
const token = await getToken({
req,
});
if (!token || !token.email) {
return false;
}
const subscription = await prisma.subscription.findFirst({
where: {
User: {
email: token.email,
},
},
});
return subscription !== null && subscription.status !== SubscriptionStatus.INACTIVE;
};

View File

@ -1,92 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@documenso/prisma";
import { stripe } from "../client";
import { getToken } from "next-auth/jwt";
export type CheckoutSessionRequest = {
body: {
id?: string;
priceId: string;
};
};
export type CheckoutSessionResponse =
| {
success: false;
message: string;
}
| {
success: true;
url: string;
};
export const checkoutSessionHandler = async (req: NextApiRequest, res: NextApiResponse) => {
if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
return res.status(500).json({
success: false,
message: "Subscriptions are not enabled",
});
}
if (req.method !== "POST") {
return res.status(405).json({
success: false,
message: "Method not allowed",
});
}
const token = await getToken({
req,
});
if (!token || !token.email) {
return res.status(401).json({
success: false,
message: "Unauthorized",
});
}
const user = await prisma.user.findFirst({
where: {
email: token.email,
},
});
if (!user) {
return res.status(404).json({
success: false,
message: "No user found",
});
}
const { id, priceId } = req.body;
if (typeof priceId !== "string") {
return res.status(400).json({
success: false,
message: "No id or priceId found in request",
});
}
const session = await stripe.checkout.sessions.create({
customer: id,
customer_email: user.email,
client_reference_id: String(user.id),
payment_method_types: ["card"],
line_items: [
{
price: priceId,
quantity: 1,
},
],
mode: "subscription",
allow_promotion_codes: true,
success_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing?canceled=true`,
});
return res.status(200).json({
success: true,
url: session.url,
});
};

View File

@ -1,63 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@documenso/prisma";
import { Subscription } from "@prisma/client";
import { getToken } from "next-auth/jwt";
export type GetSubscriptionRequest = never;
export type GetSubscriptionResponse =
| {
success: false;
message: string;
}
| {
success: true;
subscription: Subscription;
};
export const getSubscriptionHandler = async (req: NextApiRequest, res: NextApiResponse) => {
if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
return res.status(500).json({
success: false,
message: "Subscriptions are not enabled",
});
}
if (req.method !== "GET") {
return res.status(405).json({
success: false,
message: "Method not allowed",
});
}
const token = await getToken({
req,
});
if (!token || !token.email) {
return res.status(401).json({
success: false,
message: "Unauthorized",
});
}
const subscription = await prisma.subscription.findFirst({
where: {
User: {
email: token.email,
},
},
});
if (!subscription) {
return res.status(404).json({
success: false,
message: "No subscription found",
});
}
return res.status(200).json({
success: true,
subscription,
});
};

View File

@ -1,54 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { stripe } from "../client";
export type PortalSessionRequest = {
body: {
id: string;
};
};
export type PortalSessionResponse =
| {
success: false;
message: string;
}
| {
success: true;
url: string;
};
export const portalSessionHandler = async (req: NextApiRequest, res: NextApiResponse<PortalSessionResponse>) => {
if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
return res.status(500).json({
success: false,
message: "Subscriptions are not enabled",
});
}
if (req.method !== "POST") {
return res.status(405).json({
success: false,
message: "Method not allowed",
});
}
const { id } = req.body;
if (typeof id !== "string") {
return res.status(400).json({
success: false,
message: "No id found in request",
});
}
const session = await stripe.billingPortal.sessions.create({
customer: id,
return_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
});
return res.status(200).json({
success: true,
url: session.url,
});
};

View File

@ -1,201 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@documenso/prisma";
import { stripe } from "../client";
import { SubscriptionStatus } from "@prisma/client";
import { buffer } from "micro";
import Stripe from "stripe";
const log = (...args: any[]) => console.log("[stripe]", ...args);
export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
return res.status(500).json({
success: false,
message: "Subscriptions are not enabled",
});
}
const sig =
typeof req.headers["stripe-signature"] === "string" ? req.headers["stripe-signature"] : "";
if (!sig) {
return res.status(400).json({
success: false,
message: "No signature found in request",
});
}
const body = await buffer(req);
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
log("event-type:", event.type);
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
const customerId =
typeof subscription.customer === "string" ? subscription.customer : subscription.customer?.id;
await prisma.subscription.upsert({
where: {
customerId,
},
create: {
customerId,
status: SubscriptionStatus.ACTIVE,
planId: subscription.id,
priceId: subscription.items.data[0].price.id,
periodEnd: new Date(subscription.current_period_end * 1000),
userId: Number(session.client_reference_id as string),
},
update: {
customerId,
status: SubscriptionStatus.ACTIVE,
planId: subscription.id,
priceId: subscription.items.data[0].price.id,
periodEnd: new Date(subscription.current_period_end * 1000),
},
});
return res.status(200).json({
success: true,
message: "Webhook received",
});
}
if (event.type === "invoice.payment_succeeded") {
const invoice = event.data.object as Stripe.Invoice;
if (invoice.billing_reason !== "subscription_cycle") {
return res.status(200).json({
success: true,
message: "Webhook received",
});
}
const customerId =
typeof invoice.customer === "string" ? invoice.customer : invoice.customer?.id;
const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string);
const hasSubscription = await prisma.subscription.findFirst({
where: {
customerId,
},
});
if (hasSubscription) {
await prisma.subscription.update({
where: {
customerId,
},
data: {
status: SubscriptionStatus.ACTIVE,
planId: subscription.id,
priceId: subscription.items.data[0].price.id,
periodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
return res.status(200).json({
success: true,
message: "Webhook received",
});
}
if (event.type === "invoice.payment_failed") {
const failedInvoice = event.data.object as Stripe.Invoice;
const customerId = failedInvoice.customer as string;
const hasSubscription = await prisma.subscription.findFirst({
where: {
customerId,
},
});
if (hasSubscription) {
await prisma.subscription.update({
where: {
customerId,
},
data: {
status: SubscriptionStatus.PAST_DUE,
},
});
}
return res.status(200).json({
success: true,
message: "Webhook received",
});
}
if (event.type === "customer.subscription.updated") {
const updatedSubscription = event.data.object as Stripe.Subscription;
const customerId = updatedSubscription.customer as string;
const hasSubscription = await prisma.subscription.findFirst({
where: {
customerId,
},
});
if (hasSubscription) {
await prisma.subscription.update({
where: {
customerId,
},
data: {
status: SubscriptionStatus.ACTIVE,
planId: updatedSubscription.id,
priceId: updatedSubscription.items.data[0].price.id,
periodEnd: new Date(updatedSubscription.current_period_end * 1000),
},
});
}
return res.status(200).json({
success: true,
message: "Webhook received",
});
}
if (event.type === "customer.subscription.deleted") {
const deletedSubscription = event.data.object as Stripe.Subscription;
const customerId = deletedSubscription.customer as string;
const hasSubscription = await prisma.subscription.findFirst({
where: {
customerId,
},
});
if (hasSubscription) {
await prisma.subscription.update({
where: {
customerId,
},
data: {
status: SubscriptionStatus.INACTIVE,
},
});
}
return res.status(200).json({
success: true,
message: "Webhook received",
});
}
log("Unhandled webhook event", event.type);
return res.status(400).json({
success: false,
message: "Unhandled webhook event",
});
};

View File

@ -1,6 +0,0 @@
export * from './data/plans'
export * from './fetchers/checkout-session'
export * from './fetchers/get-subscription'
export * from './fetchers/portal-session'
export * from './guards/subscriptions'
export * from './providers/subscription-provider'

View File

@ -1,89 +0,0 @@
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
import { fetchSubscription } from "../fetchers/get-subscription";
import { Subscription, SubscriptionStatus } from "@prisma/client";
import { useSession } from "next-auth/react";
export type SubscriptionContextValue = {
subscription: Subscription | null;
hasSubscription: boolean;
isLoading: boolean;
};
const SubscriptionContext = createContext<SubscriptionContextValue>({
subscription: null,
hasSubscription: false,
isLoading: false,
});
export const useSubscription = () => {
const context = useContext(SubscriptionContext);
if (!context) {
throw new Error(`useSubscription must be used within a SubscriptionProvider`);
}
return context;
};
export interface SubscriptionProviderProps {
children: React.ReactNode;
initialSubscription?: Subscription;
}
export const SubscriptionProvider = ({
children,
initialSubscription,
}: SubscriptionProviderProps) => {
const session = useSession();
const [isLoading, setIsLoading] = useState(false);
const [subscription, setSubscription] = useState<Subscription | null>(
initialSubscription || null
);
const hasSubscription = useMemo(() => {
console.log({
"process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS": process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS,
enabled: process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true",
"subscription.status": subscription?.status,
"subscription.periodEnd": subscription?.periodEnd,
});
if (process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true") {
return (
subscription?.status === SubscriptionStatus.ACTIVE &&
!!subscription?.periodEnd &&
new Date(subscription.periodEnd) > new Date()
);
}
return true;
}, [subscription]);
useEffect(() => {
if (process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true" && session.data) {
setIsLoading(true);
fetchSubscription().then((res) => {
if (res.success) {
setSubscription(res.subscription);
} else {
setSubscription(null);
}
setIsLoading(false);
});
}
}, [session.data]);
return (
<SubscriptionContext.Provider
value={{
subscription,
hasSubscription,
isLoading,
}}>
{children}
</SubscriptionContext.Provider>
);
};

View File

@ -0,0 +1,7 @@
export type FindResultSet<T> = {
data: T[];
count: number;
currentPage: number;
perPage: number;
totalPages: number;
};

View File

@ -0,0 +1,5 @@
import { DocumentStatus } from '@documenso/prisma/client';
export const isDocumentStatus = (value: unknown): value is DocumentStatus => {
return Object.values(DocumentStatus).includes(value as DocumentStatus);
};

View File

@ -0,0 +1,16 @@
/* eslint-disable turbo/no-undeclared-env-vars */
export const getBaseUrl = () => {
if (typeof window !== 'undefined') {
return '';
}
if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`;
}
if (process.env.NEXT_PUBLIC_SITE_URL) {
return `https://${process.env.NEXT_PUBLIC_SITE_URL}`;
}
return `http://localhost:${process.env.PORT ?? 3000}`;
};

View File

@ -1,21 +0,0 @@
export const localStorage = {
getItem(key: string) {
try {
return window.localStorage.getItem(key);
} catch (e) {
// In case storage is restricted. Possible reasons
// 1. Chrome/Firefox/... Incognito mode.
return null;
}
},
setItem(key: string, value: string) {
try {
window.localStorage.setItem(key, value);
} catch (e) {
// In case storage is restricted. Possible reasons
// 1. Chrome/Firefox/... Incognito mode.
// 2. Storage limit reached
return;
}
},
};

View File

@ -1,2 +0,0 @@
export { insertTextInPDF } from "./insertTextInPDF";
export { insertImageInPDF } from "./insertImageInPDF";

View File

@ -1,7 +0,0 @@
{
"name": "@documenso/pdf",
"version": "0.0.0",
"private": true,
"main": "index.ts",
"dependencies": {}
}

View File

@ -0,0 +1,41 @@
/** @type {import('prettier').Config} */
module.exports = {
arrowParens: 'always',
printWidth: 100,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'all',
importOrder: [
'^server-only|client-only$',
'^react$',
'^next(/.*)?$',
'<THIRD_PARTY_MODULES>',
'^@documenso/(.*)$',
'^~/(.*)$',
'^[./]',
],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
// !: Waiting for these to make it upstream
// importOrderMergeDuplicateImports: true,
// importOrderCombineTypeAndValueImports: true,
plugins: [
'@trivago/prettier-plugin-sort-imports',
'prettier-plugin-sql',
'prettier-plugin-tailwindcss',
],
overrides: [
{
files: ['*.sql'],
options: {
language: 'postgresql',
keywordCase: 'upper',
expressionWidth: 60,
},
},
],
};

View File

@ -0,0 +1,13 @@
{
"name": "@documenso/prettier-config",
"version": "0.0.0",
"main": "./index.cjs",
"license": "MIT",
"dependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"prettier": "^2.8.8",
"prettier-plugin-sql": "^0.14.0",
"prettier-plugin-tailwindcss": "^0.2.8"
},
"devDependencies": {}
}

View File

@ -1,3 +0,0 @@
node_modules
# Keep environment variables out of version control
.env

View File

@ -0,0 +1 @@
export * from '@prisma/client';

View File

@ -1,16 +1,15 @@
import { isENVProd } from "@documenso/lib";
import { Document, PrismaClient, User } from "@prisma/client";
import { PrismaClient } from '@prisma/client';
declare global {
var client: PrismaClient | undefined;
// We need `var` to declare a global variable in TypeScript
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
}
// Instanciate new client if non exists
const prisma = globalThis.client || new PrismaClient();
// Save for reuse in dev environment to avoid many client instances in dev where restart and reloads
if (!isENVProd) {
globalThis.client = prisma;
if (!globalThis.prisma) {
globalThis.prisma = new PrismaClient();
}
export default prisma;
export const prisma = globalThis.prisma || new PrismaClient();
export const getPrismaClient = () => prisma;

View File

@ -1,92 +0,0 @@
{
"name": "@documenso/prisma",
"version": "0.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@documenso/prisma",
"version": "0.0.0",
"dependencies": {
"@prisma/client": "^4.8.0",
"prisma": "^4.8.0"
},
"devDependencies": {}
},
"node_modules/@prisma/client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.8.1.tgz",
"integrity": "sha512-d4xhZhETmeXK/yZ7K0KcVOzEfI5YKGGEr4F5SBV04/MU4ncN/HcE28sy3e4Yt8UFW0ZuImKFQJE+9rWt9WbGSQ==",
"hasInstallScript": true,
"dependencies": {
"@prisma/engines-version": "4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe"
},
"engines": {
"node": ">=14.17"
},
"peerDependencies": {
"prisma": "*"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
}
}
},
"node_modules/@prisma/engines": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.8.1.tgz",
"integrity": "sha512-93tctjNXcIS+i/e552IO6tqw17sX8liivv8WX9lDMCpEEe3ci+nT9F+1oHtAafqruXLepKF80i/D20Mm+ESlOw==",
"hasInstallScript": true
},
"node_modules/@prisma/engines-version": {
"version": "4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe.tgz",
"integrity": "sha512-MHSOSexomRMom8QN4t7bu87wPPD+pa+hW9+71JnVcF3DqyyO/ycCLhRL1we3EojRpZxKvuyGho2REQsMCvxcJw=="
},
"node_modules/prisma": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.8.1.tgz",
"integrity": "sha512-ZMLnSjwulIeYfaU1O6/LF6PEJzxN5par5weykxMykS9Z6ara/j76JH3Yo2AH3bgJbPN4Z6NeCK9s5fDkzf33cg==",
"hasInstallScript": true,
"dependencies": {
"@prisma/engines": "4.8.1"
},
"bin": {
"prisma": "build/index.js",
"prisma2": "build/index.js"
},
"engines": {
"node": ">=14.17"
}
}
},
"dependencies": {
"@prisma/client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.8.1.tgz",
"integrity": "sha512-d4xhZhETmeXK/yZ7K0KcVOzEfI5YKGGEr4F5SBV04/MU4ncN/HcE28sy3e4Yt8UFW0ZuImKFQJE+9rWt9WbGSQ==",
"requires": {
"@prisma/engines-version": "4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe"
}
},
"@prisma/engines": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.8.1.tgz",
"integrity": "sha512-93tctjNXcIS+i/e552IO6tqw17sX8liivv8WX9lDMCpEEe3ci+nT9F+1oHtAafqruXLepKF80i/D20Mm+ESlOw=="
},
"@prisma/engines-version": {
"version": "4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe.tgz",
"integrity": "sha512-MHSOSexomRMom8QN4t7bu87wPPD+pa+hW9+71JnVcF3DqyyO/ycCLhRL1we3EojRpZxKvuyGho2REQsMCvxcJw=="
},
"prisma": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.8.1.tgz",
"integrity": "sha512-ZMLnSjwulIeYfaU1O6/LF6PEJzxN5par5weykxMykS9Z6ara/j76JH3Yo2AH3bgJbPN4Z6NeCK9s5fDkzf33cg==",
"requires": {
"@prisma/engines": "4.8.1"
}
}
}
}

View File

@ -1,22 +1,18 @@
{
"name": "@documenso/prisma",
"version": "0.0.0",
"private": true,
"main": "index.ts",
"version": "1.0.0",
"main": "./index.ts",
"types": "./index.ts",
"license": "MIT",
"scripts": {
"db-studio": "prisma studio",
"db-seed": "prisma db seed"
"build": "prisma generate",
"format": "prisma format",
"prisma:generate": "prisma generate",
"prisma:migrate-dev": "prisma migrate dev",
"prisma:migrate-deploy": "prisma migrate deploy"
},
"dependencies": {
"@prisma/client": "^4.8.1",
"prisma": "^4.8.1"
},
"devDependencies": {
"@types/node": "^18.11.18",
"ts-node": "^10.9.1",
"typescript": "4.8.4"
},
"prisma": {
"seed": "ts-node --transpile-only ./seed.ts"
"@prisma/client": "^4.14.0",
"prisma": "^4.14.0"
}
}
}

View File

@ -4,7 +4,7 @@ generator client {
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
url = env("NEXT_PRIVATE_DATABASE_URL")
}
enum IdentityProvider {

File diff suppressed because one or more lines are too long

View File

@ -1,55 +0,0 @@
const { PDFArray, CharCodes } = require("pdf-lib");
/**
* Extends PDFArray class in order to make ByteRange look like this:
* /ByteRange [0 /********** /********** /**********]
* Not this:
* /ByteRange [ 0 /********** /********** /********** ]
*/
class PDFArrayCustom extends PDFArray {
static withContext(context) {
return new PDFArrayCustom(context);
}
clone(context) {
const clone = PDFArrayCustom.withContext(context || this.context);
for (let idx = 0, len = this.size(); idx < len; idx++) {
clone.push(this.array[idx]);
}
return clone;
}
toString() {
let arrayString = "[";
for (let idx = 0, len = this.size(); idx < len; idx++) {
arrayString += this.get(idx).toString();
if (idx < len - 1) arrayString += " ";
}
arrayString += "]";
return arrayString;
}
sizeInBytes() {
let size = 2;
for (let idx = 0, len = this.size(); idx < len; idx++) {
size += this.get(idx).sizeInBytes();
if (idx < len - 1) size += 1;
}
return size;
}
copyBytesInto(buffer, offset) {
const initialOffset = offset;
buffer[offset++] = CharCodes.LeftSquareBracket;
for (let idx = 0, len = this.size(); idx < len; idx++) {
offset += this.get(idx).copyBytesInto(buffer, offset);
if (idx < len - 1) buffer[offset++] = CharCodes.Space;
}
buffer[offset++] = CharCodes.RightSquareBracket;
return offset - initialOffset;
}
}
module.exports = PDFArrayCustom;

View File

@ -1,73 +0,0 @@
import fs from "fs";
import { PDFDocument, PDFHexString, PDFName, PDFNumber, PDFString } from "pdf-lib";
// 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 package.
const signer = require("./node-signpdf/dist/signpdf");
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");
const p12Buffer = Buffer.from(
fs
.readFileSync(process.env.CERT_FILE_PATH || "resources/certificate.p12")
.toString(process.env.CERT_FILE_ENCODING ? undefined : "binary"),
(process.env.CERT_FILE_ENCODING as BufferEncoding) || "binary"
);
const SIGNATURE_LENGTH = p12Buffer.length * 2;
const pdfDoc = await PDFDocument.load(pdfBuffer);
const pages = pdfDoc.getPages();
const ByteRange = PDFArrayCustom.withContext(pdfDoc.context);
ByteRange.push(PDFNumber.of(0));
ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER));
ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER));
ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER));
const signatureDict = pdfDoc.context.obj({
Type: "Sig",
Filter: "Adobe.PPKLite",
SubFilter: "adbe.pkcs7.detached",
ByteRange,
Contents: PDFHexString.of("A".repeat(SIGNATURE_LENGTH)),
Reason: PDFString.of("Signed by Documenso"),
M: PDFString.fromDate(new Date()),
});
const signatureDictRef = pdfDoc.context.register(signatureDict);
const widgetDict = pdfDoc.context.obj({
Type: "Annot",
Subtype: "Widget",
FT: "Sig",
Rect: [0, 0, 0, 0],
V: signatureDictRef,
T: PDFString.of("Signature1"),
F: 4,
P: pages[0].ref,
});
const widgetDictRef = pdfDoc.context.register(widgetDict);
// Add signature widget to the first page
pages[0].node.set(PDFName.of("Annots"), pdfDoc.context.obj([widgetDictRef]));
// Create an AcroForm object containing the signature widget
pdfDoc.catalog.set(
PDFName.of("AcroForm"),
pdfDoc.context.obj({
SigFlags: 3,
Fields: [widgetDictRef],
})
);
const modifiedPdfBytes = await pdfDoc.save({ useObjectStreams: false });
const modifiedPdfBuffer = Buffer.from(modifiedPdfBytes);
const signObj = new signer.SignPdf();
const signedPdfBuffer: Buffer = signObj.sign(modifiedPdfBuffer, p12Buffer, {
passphrase: process.env.CERT_PASSPHRASE || "",
});
return signedPdfBuffer.toString("base64");
};

View File

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

View File

@ -1,33 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
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;
exports.ERROR_TYPE_INPUT = ERROR_TYPE_INPUT;
const ERROR_TYPE_PARSE = 3;
exports.ERROR_TYPE_PARSE = ERROR_TYPE_PARSE;
const ERROR_VERIFY_SIGNATURE = 4;
exports.ERROR_VERIFY_SIGNATURE = ERROR_VERIFY_SIGNATURE;
class SignPdfError extends Error {
constructor(msg, type = ERROR_TYPE_UNKNOWN) {
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;

View File

@ -1,24 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
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;
const DEFAULT_SIGNATURE_LENGTH = 8192;
exports.DEFAULT_SIGNATURE_LENGTH = DEFAULT_SIGNATURE_LENGTH;
const DEFAULT_BYTE_RANGE_PLACEHOLDER = "**********";
exports.DEFAULT_BYTE_RANGE_PLACEHOLDER = DEFAULT_BYTE_RANGE_PLACEHOLDER;
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";
exports.SUBFILTER_ADOBE_PKCS7_SHA1 = SUBFILTER_ADOBE_PKCS7_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;

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