mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 01:01:49 +10:00
wip: refresh design
This commit is contained in:
42
packages/eslint-config/index.cjs
Normal file
42
packages/eslint-config/index.cjs
Normal 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' }],
|
||||
},
|
||||
};
|
||||
17
packages/eslint-config/package.json
Normal file
17
packages/eslint-config/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export { createField } from "./createField";
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -1,2 +0,0 @@
|
||||
export { uploadDocument } from "./uploadDocument";
|
||||
export { updateUser } from "./updateUser";
|
||||
@ -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": {}
|
||||
}
|
||||
@ -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 :/",
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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) {}
|
||||
};
|
||||
@ -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) {}
|
||||
};
|
||||
@ -1,5 +0,0 @@
|
||||
export const deleteDocument = (documentId: number): Promise<Response> => {
|
||||
return fetch(`/api/documents/${documentId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
};
|
||||
@ -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) {}
|
||||
};
|
||||
@ -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",
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
export const getDocuments = (): Promise<Response> => {
|
||||
return fetch("/api/documents", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
export const getUser = (): Promise<Response> => {
|
||||
return fetch("/api/users/me");
|
||||
};
|
||||
@ -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";
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -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 :/",
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -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",
|
||||
});
|
||||
};
|
||||
@ -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",
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export default function classNames(...classes: unknown[]) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
21
packages/lib/client-only/hooks/use-update-search-params.ts
Normal file
21
packages/lib/client-only/hooks/use-update-search-params.ts
Normal 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()}`);
|
||||
};
|
||||
};
|
||||
@ -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();
|
||||
@ -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;
|
||||
1
packages/lib/constants/auth.ts
Normal file
1
packages/lib/constants/auth.ts
Normal file
@ -0,0 +1 @@
|
||||
export const SALT_ROUNDS = 12;
|
||||
5
packages/lib/constants/features.ts
Normal file
5
packages/lib/constants/features.ts
Normal 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';
|
||||
@ -1 +0,0 @@
|
||||
export const isENVProd = process.env.NODE_ENV === "production";
|
||||
5
packages/lib/errors/user-exists.ts
Normal file
5
packages/lib/errors/user-exists.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export class UserExistsError extends Error {
|
||||
constructor() {
|
||||
super('User already exists');
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export * from './strings';
|
||||
@ -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)}`;
|
||||
};
|
||||
@ -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 {};
|
||||
|
||||
@ -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;
|
||||
@ -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";
|
||||
@ -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;
|
||||
});
|
||||
};
|
||||
@ -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"),
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
@ -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 },
|
||||
});
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
59
packages/lib/next-auth/auth-options.ts
Normal file
59
packages/lib/next-auth/auth-options.ts
Normal 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,
|
||||
// };
|
||||
// },
|
||||
// },
|
||||
};
|
||||
54
packages/lib/next-auth/get-server-session.ts
Normal file
54
packages/lib/next-auth/get-server-session.ts
Normal 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;
|
||||
};
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
packages/lib/process-env.d.ts
vendored
24
packages/lib/process-env.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
@ -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: "" }));
|
||||
};
|
||||
@ -1,2 +0,0 @@
|
||||
export { getDocumentsForUserFromToken } from "./getDocumentsForUserFromToken";
|
||||
export { getDocument } from "./getDocument";
|
||||
10
packages/lib/server-only/auth/hash.ts
Normal file
10
packages/lib/server-only/auth/hash.ts
Normal 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);
|
||||
};
|
||||
66
packages/lib/server-only/document/find-documents.ts
Normal file
66
packages/lib/server-only/document/find-documents.ts
Normal 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),
|
||||
};
|
||||
};
|
||||
15
packages/lib/server-only/document/get-document-by-id.ts
Normal file
15
packages/lib/server-only/document/get-document-by-id.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
};
|
||||
30
packages/lib/server-only/document/get-stats.ts
Normal file
30
packages/lib/server-only/document/get-stats.ts
Normal 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;
|
||||
};
|
||||
11
packages/lib/server-only/headers/get-locale.tsx
Normal file
11
packages/lib/server-only/headers/get-locale.tsx
Normal 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;
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
8
packages/lib/server-only/redis/index.ts
Normal file
8
packages/lib/server-only/redis/index.ts
Normal 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 ?? '',
|
||||
});
|
||||
9
packages/lib/server-only/stripe/index.ts
Normal file
9
packages/lib/server-only/stripe/index.ts
Normal 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 };
|
||||
35
packages/lib/server-only/user/create-user.ts
Normal file
35
packages/lib/server-only/user/create-user.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
};
|
||||
13
packages/lib/server-only/user/get-user-by-email.ts
Normal file
13
packages/lib/server-only/user/get-user-by-email.ts
Normal 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(),
|
||||
},
|
||||
});
|
||||
};
|
||||
13
packages/lib/server-only/user/get-user-by-id.ts
Normal file
13
packages/lib/server-only/user/get-user-by-id.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
};
|
||||
32
packages/lib/server-only/user/update-password.ts
Normal file
32
packages/lib/server-only/user/update-password.ts
Normal 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;
|
||||
};
|
||||
33
packages/lib/server-only/user/update-profile.ts
Normal file
33
packages/lib/server-only/user/update-profile.ts
Normal 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;
|
||||
};
|
||||
@ -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" });
|
||||
}
|
||||
};
|
||||
@ -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 });
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -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.`,
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
@ -1,7 +0,0 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
|
||||
export const stripe = new Stripe(process.env.STRIPE_API_KEY!, {
|
||||
apiVersion: "2022-11-15",
|
||||
typescript: true,
|
||||
});
|
||||
@ -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 ?? "",
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
@ -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",
|
||||
});
|
||||
};
|
||||
@ -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'
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
7
packages/lib/types/find-result-set.ts
Normal file
7
packages/lib/types/find-result-set.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export type FindResultSet<T> = {
|
||||
data: T[];
|
||||
count: number;
|
||||
currentPage: number;
|
||||
perPage: number;
|
||||
totalPages: number;
|
||||
};
|
||||
5
packages/lib/types/is-document-status.ts
Normal file
5
packages/lib/types/is-document-status.ts
Normal 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);
|
||||
};
|
||||
16
packages/lib/universal/get-base-url.ts
Normal file
16
packages/lib/universal/get-base-url.ts
Normal 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}`;
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -1,2 +0,0 @@
|
||||
export { insertTextInPDF } from "./insertTextInPDF";
|
||||
export { insertImageInPDF } from "./insertImageInPDF";
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"name": "@documenso/pdf",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"dependencies": {}
|
||||
}
|
||||
41
packages/prettier-config/index.cjs
Normal file
41
packages/prettier-config/index.cjs
Normal 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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
13
packages/prettier-config/package.json
Normal file
13
packages/prettier-config/package.json
Normal 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": {}
|
||||
}
|
||||
3
packages/prisma/.gitignore
vendored
3
packages/prisma/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
node_modules
|
||||
# Keep environment variables out of version control
|
||||
.env
|
||||
1
packages/prisma/client.ts
Normal file
1
packages/prisma/client.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from '@prisma/client';
|
||||
@ -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;
|
||||
|
||||
92
packages/prisma/package-lock.json
generated
92
packages/prisma/package-lock.json
generated
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
@ -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;
|
||||
@ -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");
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export { signDocument } from "./signDocument";
|
||||
@ -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;
|
||||
@ -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
Reference in New Issue
Block a user