Merge branch 'main' of https://github.com/documenso/documenso into feat/redirect-templates

This commit is contained in:
Adithya Krishna
2024-05-06 11:32:34 +05:30
15 changed files with 125 additions and 59 deletions

View File

@ -8,7 +8,7 @@ import { useRouter } from 'next/navigation';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { base64 } from '@documenso/lib/universal/base64'; import { base64 } from '@documenso/lib/universal/base64';
import { putFile } from '@documenso/lib/universal/upload/put-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import type { Field, Recipient } from '@documenso/prisma/client'; import type { Field, Recipient } from '@documenso/prisma/client';
import { DocumentDataType, Prisma } from '@documenso/prisma/client'; import { DocumentDataType, Prisma } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
@ -115,7 +115,7 @@ export const SinglePlayerClient = () => {
} }
try { try {
const putFileData = await putFile(uploadedFile.file); const putFileData = await putPdfFile(uploadedFile.file);
const documentToken = await createSinglePlayerDocument({ const documentToken = await createSinglePlayerDocument({
documentData: { documentData: {

View File

@ -10,8 +10,9 @@ import { useSession } from 'next-auth/react';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { putFile } from '@documenso/lib/universal/upload/put-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { TRPCClientError } from '@documenso/trpc/client'; import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
@ -57,7 +58,7 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
try { try {
setIsLoading(true); setIsLoading(true);
const { type, data } = await putFile(file); const { type, data } = await putPdfFile(file);
const { id: documentDataId } = await createDocumentData({ const { id: documentDataId } = await createDocumentData({
type, type,
@ -83,13 +84,21 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
}); });
router.push(`${formatDocumentsPath(team?.url)}/${id}/edit`); router.push(`${formatDocumentsPath(team?.url)}/${id}/edit`);
} catch (error) { } catch (err) {
console.error(error); const error = AppError.parseError(err);
if (error instanceof TRPCClientError) { console.error(err);
if (error.code === 'INVALID_DOCUMENT_FILE') {
toast({
title: 'Invalid file',
description: 'You cannot upload encrypted PDFs',
variant: 'destructive',
});
} else if (err instanceof TRPCClientError) {
toast({ toast({
title: 'Error', title: 'Error',
description: error.message, description: err.message,
variant: 'destructive', variant: 'destructive',
}); });
} else { } else {

View File

@ -12,7 +12,7 @@ import * as z from 'zod';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { base64 } from '@documenso/lib/universal/base64'; import { base64 } from '@documenso/lib/universal/base64';
import { putFile } from '@documenso/lib/universal/upload/put-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -98,7 +98,7 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
const file: File = uploadedFile.file; const file: File = uploadedFile.file;
try { try {
const { type, data } = await putFile(file); const { type, data } = await putPdfFile(file);
const { id: templateDocumentDataId } = await createDocumentData({ const { id: templateDocumentDataId } = await createDocumentData({
type, type,

View File

@ -2,6 +2,8 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import NextAuth from 'next-auth'; import NextAuth from 'next-auth';
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options'; import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@ -18,15 +20,29 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
error: '/signin', error: '/signin',
}, },
events: { events: {
signIn: async ({ user }) => { signIn: async ({ user: { id: userId } }) => {
const [user] = await Promise.all([
await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
}),
await prisma.userSecurityAuditLog.create({ await prisma.userSecurityAuditLog.create({
data: { data: {
userId: user.id, userId,
ipAddress, ipAddress,
userAgent, userAgent,
type: UserSecurityAuditLogType.SIGN_IN, type: UserSecurityAuditLogType.SIGN_IN,
}, },
}),
]);
// Create the Stripe customer and attach it to the user if it doesn't exist.
if (user.customerId === null && IS_BILLING_ENABLED()) {
await getStripeCustomerByUser(user).catch((err) => {
console.error(err);
}); });
}
}, },
signOut: async ({ token }) => { signOut: async ({ token }) => {
const userId = typeof token.id === 'string' ? parseInt(token.id) : token.id; const userId = typeof token.id === 'string' ? parseInt(token.id) : token.id;

View File

@ -22,7 +22,7 @@ import { updateRecipient } from '@documenso/lib/server-only/recipient/update-rec
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { getFile } from '@documenso/lib/universal/upload/get-file'; import { getFile } from '@documenso/lib/universal/upload/get-file';
import { putFile } from '@documenso/lib/universal/upload/put-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { import {
getPresignGetUrl, getPresignGetUrl,
getPresignPostUrl, getPresignPostUrl,
@ -303,7 +303,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
formValues: body.formValues, formValues: body.formValues,
}); });
const newDocumentData = await putFile({ const newDocumentData = await putPdfFile({
name: fileName, name: fileName,
type: 'application/pdf', type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled), arrayBuffer: async () => Promise.resolve(prefilled),

View File

@ -5,7 +5,7 @@ import { sealDocument } from '@documenso/lib/server-only/document/seal-document'
import { redis } from '@documenso/lib/server-only/redis'; import { redis } from '@documenso/lib/server-only/redis';
import { stripe } from '@documenso/lib/server-only/stripe'; import { stripe } from '@documenso/lib/server-only/stripe';
import { alphaid, nanoid } from '@documenso/lib/universal/id'; import { alphaid, nanoid } from '@documenso/lib/universal/id';
import { putFile } from '@documenso/lib/universal/upload/put-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { import {
DocumentStatus, DocumentStatus,
@ -74,7 +74,7 @@ export const onEarlyAdoptersCheckout = async ({ session }: OnEarlyAdoptersChecko
new URL('@documenso/assets/documenso-supporter-pledge.pdf', import.meta.url), new URL('@documenso/assets/documenso-supporter-pledge.pdf', import.meta.url),
).then(async (res) => res.arrayBuffer()); ).then(async (res) => res.arrayBuffer());
const { id: documentDataId } = await putFile({ const { id: documentDataId } = await putPdfFile({
name: 'Documenso Supporter Pledge.pdf', name: 'Documenso Supporter Pledge.pdf',
type: 'application/pdf', type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(documentBuffer), arrayBuffer: async () => Promise.resolve(documentBuffer),

View File

@ -21,6 +21,7 @@ export const FEATURE_FLAG_POLL_INTERVAL = 30000;
* Does not take any person or group properties into account. * Does not take any person or group properties into account.
*/ */
export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = { export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
app_allow_encrypted_documents: false,
app_billing: NEXT_PUBLIC_FEATURE_BILLING_ENABLED() === 'true', app_billing: NEXT_PUBLIC_FEATURE_BILLING_ENABLED() === 'true',
app_document_page_view_history_sheet: false, app_document_page_view_history_sheet: false,
app_passkey: WEBAPP_BASE_URL === 'http://localhost:3000', // Temp feature flag. app_passkey: WEBAPP_BASE_URL === 'http://localhost:3000', // Temp feature flag.

View File

@ -137,7 +137,7 @@ export const completeDocumentWithToken = async ({
await sendPendingEmail({ documentId, recipientId: recipient.id }); await sendPendingEmail({ documentId, recipientId: recipient.id });
} }
const documents = await prisma.document.updateMany({ const haveAllRecipientsSigned = await prisma.document.findFirst({
where: { where: {
id: document.id, id: document.id,
Recipient: { Recipient: {
@ -146,13 +146,9 @@ export const completeDocumentWithToken = async ({
}, },
}, },
}, },
data: {
status: DocumentStatus.COMPLETED,
completedAt: new Date(),
},
}); });
if (documents.count > 0) { if (haveAllRecipientsSigned) {
await sealDocument({ documentId: document.id, requestMetadata }); await sealDocument({ documentId: document.id, requestMetadata });
} }

View File

@ -75,17 +75,19 @@ export const deleteDocument = async ({
} }
// Continue to hide the document from the user if they are a recipient. // Continue to hide the document from the user if they are a recipient.
// Dirty way of doing this but it's faster than refetching the document.
if (userRecipient?.documentDeletedAt === null) { if (userRecipient?.documentDeletedAt === null) {
await prisma.recipient.update({ await prisma.recipient
.update({
where: { where: {
documentId_email: { id: userRecipient.id,
documentId: document.id,
email: user.email,
},
}, },
data: { data: {
documentDeletedAt: new Date().toISOString(), documentDeletedAt: new Date().toISOString(),
}, },
})
.catch(() => {
// Do nothing.
}); });
} }

View File

@ -14,7 +14,7 @@ import { signPdf } from '@documenso/signing';
import type { RequestMetadata } from '../../universal/extract-request-metadata'; import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFile } from '../../universal/upload/get-file'; import { getFile } from '../../universal/upload/get-file';
import { putFile } from '../../universal/upload/put-file'; import { putPdfFile } from '../../universal/upload/put-file';
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf'; import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
import { flattenAnnotations } from '../pdf/flatten-annotations'; import { flattenAnnotations } from '../pdf/flatten-annotations';
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf'; import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
@ -40,6 +40,11 @@ export const sealDocument = async ({
const document = await prisma.document.findFirstOrThrow({ const document = await prisma.document.findFirstOrThrow({
where: { where: {
id: documentId, id: documentId,
Recipient: {
every: {
signingStatus: SigningStatus.SIGNED,
},
},
}, },
include: { include: {
documentData: true, documentData: true,
@ -53,10 +58,6 @@ export const sealDocument = async ({
throw new Error(`Document ${document.id} has no document data`); throw new Error(`Document ${document.id} has no document data`);
} }
if (document.status !== DocumentStatus.COMPLETED) {
throw new Error(`Document ${document.id} has not been completed`);
}
const recipients = await prisma.recipient.findMany({ const recipients = await prisma.recipient.findMany({
where: { where: {
documentId: document.id, documentId: document.id,
@ -92,9 +93,9 @@ export const sealDocument = async ({
// !: Need to write the fields onto the document as a hard copy // !: Need to write the fields onto the document as a hard copy
const pdfData = await getFile(documentData); const pdfData = await getFile(documentData);
const certificate = await getCertificatePdf({ documentId }).then(async (doc) => const certificate = await getCertificatePdf({ documentId })
PDFDocument.load(doc), .then(async (doc) => PDFDocument.load(doc))
); .catch(() => null);
const doc = await PDFDocument.load(pdfData); const doc = await PDFDocument.load(pdfData);
@ -103,11 +104,13 @@ export const sealDocument = async ({
doc.getForm().flatten(); doc.getForm().flatten();
flattenAnnotations(doc); flattenAnnotations(doc);
if (certificate) {
const certificatePages = await doc.copyPages(certificate, certificate.getPageIndices()); const certificatePages = await doc.copyPages(certificate, certificate.getPageIndices());
certificatePages.forEach((page) => { certificatePages.forEach((page) => {
doc.addPage(page); doc.addPage(page);
}); });
}
for (const field of fields) { for (const field of fields) {
await insertFieldInPDF(doc, field); await insertFieldInPDF(doc, field);
@ -119,7 +122,7 @@ export const sealDocument = async ({
const { name, ext } = path.parse(document.title); const { name, ext } = path.parse(document.title);
const { data: newData } = await putFile({ const { data: newData } = await putPdfFile({
name: `${name}_signed${ext}`, name: `${name}_signed${ext}`,
type: 'application/pdf', type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdfBuffer), arrayBuffer: async () => Promise.resolve(pdfBuffer),
@ -138,6 +141,16 @@ export const sealDocument = async ({
} }
await prisma.$transaction(async (tx) => { await prisma.$transaction(async (tx) => {
await tx.document.update({
where: {
id: document.id,
},
data: {
status: DocumentStatus.COMPLETED,
completedAt: new Date(),
},
});
await tx.documentData.update({ await tx.documentData.update({
where: { where: {
id: documentData.id, id: documentData.id,

View File

@ -8,6 +8,7 @@ import { sealDocument } from '@documenso/lib/server-only/document/seal-document'
import { updateDocument } from '@documenso/lib/server-only/document/update-document'; import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template'; import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@ -20,7 +21,6 @@ import {
RECIPIENT_ROLE_TO_EMAIL_TYPE, RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '../../constants/recipient-roles'; } from '../../constants/recipient-roles';
import { getFile } from '../../universal/upload/get-file'; import { getFile } from '../../universal/upload/get-file';
import { putFile } from '../../universal/upload/put-file';
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf'; import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
@ -102,7 +102,7 @@ export const sendDocument = async ({
formValues: document.formValues as Record<string, string | number | boolean>, formValues: document.formValues as Record<string, string | number | boolean>,
}); });
const newDocumentData = await putFile({ const newDocumentData = await putPdfFile({
name: document.title, name: document.title,
type: 'application/pdf', type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled), arrayBuffer: async () => Promise.resolve(prefilled),

View File

@ -35,6 +35,7 @@ export const getCertificatePdf = async ({ documentId }: GetCertificatePdfOptions
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encryptedId}`, { await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encryptedId}`, {
waitUntil: 'networkidle', waitUntil: 'networkidle',
timeout: 10_000,
}); });
const result = await page.pdf({ const result = await page.pdf({

View File

@ -1,9 +1,12 @@
import { base64 } from '@scure/base'; import { base64 } from '@scure/base';
import { env } from 'next-runtime-env'; import { env } from 'next-runtime-env';
import { PDFDocument } from 'pdf-lib';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { getFlag } from '@documenso/lib/universal/get-feature-flag';
import { DocumentDataType } from '@documenso/prisma/client'; import { DocumentDataType } from '@documenso/prisma/client';
import { AppError } from '../../errors/app-error';
import { createDocumentData } from '../../server-only/document-data/create-document-data'; import { createDocumentData } from '../../server-only/document-data/create-document-data';
type File = { type File = {
@ -12,14 +15,38 @@ type File = {
arrayBuffer: () => Promise<ArrayBuffer>; arrayBuffer: () => Promise<ArrayBuffer>;
}; };
/**
* Uploads a document file to the appropriate storage location and creates
* a document data record.
*/
export const putPdfFile = async (file: File) => {
const isEncryptedDocumentsAllowed = await getFlag('app_allow_encrypted_documents').catch(
() => false,
);
// This will prevent uploading encrypted PDFs or anything that can't be opened.
if (!isEncryptedDocumentsAllowed) {
await PDFDocument.load(await file.arrayBuffer()).catch((e) => {
console.error(`PDF upload parse error: ${e.message}`);
throw new AppError('INVALID_DOCUMENT_FILE');
});
}
const { type, data } = await putFile(file);
return await createDocumentData({ type, data });
};
/**
* Uploads a file to the appropriate storage location.
*/
export const putFile = async (file: File) => { export const putFile = async (file: File) => {
const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT'); const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
const { type, data } = await match(NEXT_PUBLIC_UPLOAD_TRANSPORT) return await match(NEXT_PUBLIC_UPLOAD_TRANSPORT)
.with('s3', async () => putFileInS3(file)) .with('s3', async () => putFileInS3(file))
.otherwise(async () => putFileInDatabase(file)); .otherwise(async () => putFileInDatabase(file));
return await createDocumentData({ type, data });
}; };
const putFileInDatabase = async (file: File) => { const putFileInDatabase = async (file: File) => {

View File

@ -10,7 +10,7 @@ import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/cons
import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf'; import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf';
import { alphaid } from '@documenso/lib/universal/id'; import { alphaid } from '@documenso/lib/universal/id';
import { getFile } from '@documenso/lib/universal/upload/get-file'; import { getFile } from '@documenso/lib/universal/upload/get-file';
import { putFile } from '@documenso/lib/universal/upload/put-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { import {
DocumentStatus, DocumentStatus,
@ -86,7 +86,7 @@ export const singleplayerRouter = router({
}, },
}); });
const { id: documentDataId } = await putFile({ const { id: documentDataId } = await putPdfFile({
name: `${documentName}.pdf`, name: `${documentName}.pdf`,
type: 'application/pdf', type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(signedPdfBuffer), arrayBuffer: async () => Promise.resolve(signedPdfBuffer),

View File

@ -282,6 +282,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
<Button <Button
type="button" type="button"
className="dark:bg-muted dark:hover:bg-muted/80 bg-black/5 hover:bg-black/10" className="dark:bg-muted dark:hover:bg-muted/80 bg-black/5 hover:bg-black/10"
variant="secondary"
disabled={ disabled={
isSubmitting || getValues('signers').some((signer) => signer.email === user?.email) isSubmitting || getValues('signers').some((signer) => signer.email === user?.email)
} }