Merge branch 'main' into fix/refactor-use-template

This commit is contained in:
David Nguyen
2024-05-01 15:16:25 +07:00
committed by GitHub
11 changed files with 117 additions and 48 deletions

View File

@ -5,7 +5,7 @@ import { Button } from '@documenso/ui/primitives/button';
export default function SignatureDisclosure() { export default function SignatureDisclosure() {
return ( return (
<div> <div>
<article className="prose"> <article className="prose dark:prose-invert">
<h1>Electronic Signature Disclosure</h1> <h1>Electronic Signature Disclosure</h1>
<h2>Welcome</h2> <h2>Welcome</h2>

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

@ -3,7 +3,7 @@ import { createTrpcContext } from '@documenso/trpc/server/context';
import { appRouter } from '@documenso/trpc/server/router'; import { appRouter } from '@documenso/trpc/server/router';
export const config = { export const config = {
maxDuration: 60, maxDuration: 120,
api: { api: {
bodyParser: { bodyParser: {
sizeLimit: '50mb', sizeLimit: '50mb',

View File

@ -229,6 +229,13 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
requestMetadata: extractNextApiRequestMetadata(args.req), requestMetadata: extractNextApiRequestMetadata(args.req),
}); });
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
...body.meta,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
const recipients = await setRecipientsForDocument({ const recipients = await setRecipientsForDocument({
userId: user.id, userId: user.id,
teamId: team?.id, teamId: team?.id,
@ -324,10 +331,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
await upsertDocumentMeta({ await upsertDocumentMeta({
documentId: document.id, documentId: document.id,
userId: user.id, userId: user.id,
subject: body.meta.subject, ...body.meta,
message: body.meta.message,
dateFormat: body.meta.dateFormat,
timezone: body.meta.timezone,
requestMetadata: extractNextApiRequestMetadata(args.req), requestMetadata: extractNextApiRequestMetadata(args.req),
}); });
} }

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

@ -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);
@ -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

@ -4,6 +4,8 @@ import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render'; import { render } from '@documenso/email/render';
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite'; import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email'; import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import { sealDocument } from '@documenso/lib/server-only/document/seal-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 { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
@ -211,6 +213,31 @@ export const sendDocument = async ({
}), }),
); );
const allRecipientsHaveNoActionToTake = document.Recipient.every(
(recipient) => recipient.role === RecipientRole.CC,
);
if (allRecipientsHaveNoActionToTake) {
const updatedDocument = await updateDocument({
documentId,
userId,
teamId,
data: { status: DocumentStatus.COMPLETED },
});
await sealDocument({ documentId: updatedDocument.id, requestMetadata });
// Keep the return type the same for the `sendDocument` method
return await prisma.document.findFirstOrThrow({
where: {
id: documentId,
},
include: {
Recipient: true,
},
});
}
const updatedDocument = await prisma.$transaction(async (tx) => { const updatedDocument = await prisma.$transaction(async (tx) => {
if (document.status === DocumentStatus.DRAFT) { if (document.status === DocumentStatus.DRAFT) {
await tx.documentAuditLog.create({ await tx.documentAuditLog.create({

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

@ -17,6 +17,7 @@ export const getFlag = async (
options?: GetFlagOptions, options?: GetFlagOptions,
): Promise<TFeatureFlagValue> => { ): Promise<TFeatureFlagValue> => {
const requestHeaders = options?.requestHeaders ?? {}; const requestHeaders = options?.requestHeaders ?? {};
delete requestHeaders['content-length'];
if (!isFeatureFlagEnabled()) { if (!isFeatureFlagEnabled()) {
return LOCAL_FEATURE_FLAGS[flag] ?? true; return LOCAL_FEATURE_FLAGS[flag] ?? true;
@ -25,7 +26,7 @@ export const getFlag = async (
const url = new URL(`${APP_BASE_URL()}/api/feature-flag/get`); const url = new URL(`${APP_BASE_URL()}/api/feature-flag/get`);
url.searchParams.set('flag', flag); url.searchParams.set('flag', flag);
const response = await fetch(url, { return await fetch(url, {
headers: { headers: {
...requestHeaders, ...requestHeaders,
}, },
@ -35,9 +36,10 @@ export const getFlag = async (
}) })
.then(async (res) => res.json()) .then(async (res) => res.json())
.then((res) => ZFeatureFlagValueSchema.parse(res)) .then((res) => ZFeatureFlagValueSchema.parse(res))
.catch(() => false); .catch((err) => {
console.error(err);
return response; return LOCAL_FEATURE_FLAGS[flag] ?? false;
});
}; };
/** /**
@ -50,6 +52,7 @@ export const getAllFlags = async (
options?: GetFlagOptions, options?: GetFlagOptions,
): Promise<Record<string, TFeatureFlagValue>> => { ): Promise<Record<string, TFeatureFlagValue>> => {
const requestHeaders = options?.requestHeaders ?? {}; const requestHeaders = options?.requestHeaders ?? {};
delete requestHeaders['content-length'];
if (!isFeatureFlagEnabled()) { if (!isFeatureFlagEnabled()) {
return LOCAL_FEATURE_FLAGS; return LOCAL_FEATURE_FLAGS;
@ -67,7 +70,10 @@ export const getAllFlags = async (
}) })
.then(async (res) => res.json()) .then(async (res) => res.json())
.then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res)) .then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res))
.catch(() => LOCAL_FEATURE_FLAGS); .catch((err) => {
console.error(err);
return LOCAL_FEATURE_FLAGS;
});
}; };
/** /**
@ -89,7 +95,10 @@ export const getAllAnonymousFlags = async (): Promise<Record<string, TFeatureFla
}) })
.then(async (res) => res.json()) .then(async (res) => res.json())
.then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res)) .then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res))
.catch(() => LOCAL_FEATURE_FLAGS); .catch((err) => {
console.error(err);
return LOCAL_FEATURE_FLAGS;
});
}; };
interface GetFlagOptions { interface GetFlagOptions {

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)
} }