chore: merge main, resolve biome formatting conflicts

Merge origin/main into feat/document-file-conversion. Conflicts were
format-only (Tailwind class ordering, single-line vs multi-line) plus
two semantic merges:

- files.helpers.ts: combine main's pending-PDF download path with the
  branch's original-source-file (DOCX/PNG/JPEG) download path
- download-pdf.ts: combine main's versionToFilenameSuffix helper with
  the branch's server-provided Content-Disposition filename support
This commit is contained in:
ephraimduncan
2026-05-12 11:28:47 +00:00
1495 changed files with 22068 additions and 33465 deletions
@@ -1,3 +1,10 @@
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import { DOCUMENT_AUDIT_LOG_TYPE, RECIPIENT_DIFF_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import {
DocumentSigningOrder,
DocumentStatus,
@@ -10,25 +17,11 @@ import {
} from '@prisma/client';
import { DateTime } from 'luxon';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import {
DOCUMENT_AUDIT_LOG_TYPE,
RECIPIENT_DIFF_TYPE,
} from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { jobs } from '../../jobs/client';
import type { TRecipientAccessAuth } from '../../types/document-auth';
import { DocumentAuth } from '../../types/document-auth';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
import { extractDocumentAuthMethods } from '../../utils/document-auth';
import type { EnvelopeIdOptions } from '../../utils/envelope';
import { mapSecondaryIdToDocumentId, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
@@ -117,9 +110,7 @@ export const completeDocumentWithToken = async ({
});
if (!isRecipientsTurn) {
throw new Error(
`Recipient ${recipient.id} attempted to complete the document before it was their turn`,
);
throw new Error(`Recipient ${recipient.id} attempted to complete the document before it was their turn`);
}
}
@@ -189,9 +180,7 @@ export const completeDocumentWithToken = async ({
});
// This should be scoped to the current recipient.
const uninsertedDateFields = fields.filter(
(field) => field.type === FieldType.DATE && !field.inserted,
);
const uninsertedDateFields = fields.filter((field) => field.type === FieldType.DATE && !field.inserted);
let recipientName = recipient.name;
let recipientEmail = recipient.email;
@@ -1,22 +1,17 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import type { DocumentMeta, Envelope, Recipient, User } from '@prisma/client';
import { DocumentStatus, EnvelopeType, SendStatus, WebhookTriggerEvents } from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import type { DocumentMeta, Envelope, Recipient, User } from '@prisma/client';
import { DocumentStatus, EnvelopeType, SendStatus, WebhookTriggerEvents } from '@prisma/client';
import { createElement } from 'react';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import { isDocumentCompleted } from '../../utils/document';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
@@ -34,12 +29,7 @@ export type DeleteDocumentOptions = {
requestMetadata: ApiRequestMetadata;
};
export const deleteDocument = async ({
id,
userId,
teamId,
requestMetadata,
}: DeleteDocumentOptions) => {
export const deleteDocument = async ({ id, userId, teamId, requestMetadata }: DeleteDocumentOptions) => {
const user = await prisma.user.findUnique({
where: {
id: userId,
@@ -131,11 +121,7 @@ type HandleDocumentOwnerDeleteOptions = {
requestMetadata: ApiRequestMetadata;
};
const handleDocumentOwnerDelete = async ({
envelope,
user,
requestMetadata,
}: HandleDocumentOwnerDeleteOptions) => {
const handleDocumentOwnerDelete = async ({ envelope, user, requestMetadata }: HandleDocumentOwnerDeleteOptions) => {
if (envelope.deletedAt) {
return;
}
@@ -199,9 +185,7 @@ const handleDocumentOwnerDelete = async ({
});
});
const isEnvelopeDeleteEmailEnabled = extractDerivedDocumentEmailSettings(
envelope.documentMeta,
).documentDeleted;
const isEnvelopeDeleteEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).documentDeleted;
if (!isEnvelopeDeleteEmailEnabled) {
return deletedEnvelope;
@@ -1,6 +1,5 @@
import { type DocumentAuditLog, EnvelopeType, type Prisma } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { type DocumentAuditLog, EnvelopeType, type Prisma } from '@prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
@@ -99,7 +98,7 @@ export const findDocumentAuditLogs = async ({
}),
]);
let nextCursor: string | undefined = undefined;
let nextCursor: string | undefined;
const parsedData = data.map((auditLog) => parseDocumentAuditLogData(auditLog));
@@ -1,16 +1,20 @@
import { kyselyPrisma, prisma, sql } from '@documenso/prisma';
import type { DB } from '@documenso/prisma/generated/types';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import type { DocumentSource, Envelope, Team, TeamEmail } from '@prisma/client';
import { DocumentVisibility } from '@prisma/client';
import { DocumentStatus } from '@prisma/client';
import { EnvelopeType, RecipientRole, SigningStatus, TeamMemberRole } from '@prisma/client';
import {
DocumentStatus,
DocumentVisibility,
EnvelopeType,
RecipientRole,
SigningStatus,
TeamMemberRole,
} from '@prisma/client';
import type { Expression, ExpressionBuilder, SelectQueryBuilder, SqlBool } from 'kysely';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { kyselyPrisma, prisma, sql } from '@documenso/prisma';
import type { DB } from '@documenso/prisma/generated/types';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { type FindResultResponse } from '../../types/search-params';
import type { FindResultResponse } from '../../types/search-params';
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
import { getTeamById } from '../team/get-team';
@@ -137,18 +141,14 @@ export const findDocuments = async ({
// folder, period, sender, source, template, and search.
const buildBaseQuery = () => {
let qb = kyselyPrisma.$kysely
.selectFrom('Envelope')
.select(['Envelope.id', 'Envelope.createdAt']);
let qb = kyselyPrisma.$kysely.selectFrom('Envelope').select(['Envelope.id', 'Envelope.createdAt']);
// Type must be DOCUMENT (enum cast requires raw sql — this is the one escape hatch)
qb = qb.where('Envelope.type', '=', sql.lit(EnvelopeType.DOCUMENT));
// Folder filter
qb =
folderId !== undefined
? qb.where('Envelope.folderId', '=', folderId)
: qb.where('Envelope.folderId', 'is', null);
folderId !== undefined ? qb.where('Envelope.folderId', '=', folderId) : qb.where('Envelope.folderId', 'is', null);
// Period filter
if (period) {
@@ -220,10 +220,7 @@ export const findDocuments = async ({
eb.or([
eb('Envelope.userId', '=', user.id),
eb.and([
eb('Envelope.status', 'in', [
sql.lit(DocumentStatus.COMPLETED),
sql.lit(DocumentStatus.PENDING),
]),
eb('Envelope.status', 'in', [sql.lit(DocumentStatus.COMPLETED), sql.lit(DocumentStatus.PENDING)]),
recipientExists(eb, user.email),
]),
]),
@@ -311,10 +308,7 @@ export const findDocuments = async ({
DocumentVisibility.MANAGER_AND_ABOVE,
DocumentVisibility.ADMIN,
])
.with(TeamMemberRole.MANAGER, () => [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
])
.with(TeamMemberRole.MANAGER, () => [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE])
.otherwise(() => [DocumentVisibility.EVERYONE]);
// Visibility: meets role threshold OR directly involved
@@ -331,15 +325,11 @@ export const findDocuments = async ({
// Deleted filter for team path
const teamDeletedFilter = (eb: EnvelopeExpressionBuilder) => {
const branches = [
eb.and([eb('Envelope.teamId', '=', teamData.id), eb('Envelope.deletedAt', 'is', null)]),
];
const branches = [eb.and([eb('Envelope.teamId', '=', teamData.id), eb('Envelope.deletedAt', 'is', null)])];
if (teamEmail) {
branches.push(eb.and([senderEmailIs(eb, teamEmail), eb('Envelope.deletedAt', 'is', null)]));
branches.push(
recipientExists(eb, teamEmail, (reb) => reb('Recipient.documentDeletedAt', 'is', null)),
);
branches.push(recipientExists(eb, teamEmail, (reb) => reb('Recipient.documentDeletedAt', 'is', null)));
}
return eb.or(branches);
@@ -353,10 +343,7 @@ export const findDocuments = async ({
if (teamEmail) {
accessBranches.push(senderEmailIs(eb, teamEmail));
accessBranches.push(
eb.and([
eb('status', '!=', sql.lit(ExtendedDocumentStatus.DRAFT)),
recipientExists(eb, teamEmail),
]),
eb.and([eb('status', '!=', sql.lit(ExtendedDocumentStatus.DRAFT)), recipientExists(eb, teamEmail)]),
);
}
@@ -368,23 +355,21 @@ export const findDocuments = async ({
return null;
}
return qb
.where('Envelope.status', '!=', sql.lit(ExtendedDocumentStatus.DRAFT))
.where((eb) =>
eb.and([
visibilityFilter(eb),
// Single EXISTS check: the team-email recipient must be NOT_SIGNED,
// non-CC, and not soft-deleted. Replaces teamDeletedFilter + separate
// recipientExists, eliminating a hashed SubPlan (~79k rows).
recipientExists(eb, teamEmail, (reb) =>
reb.and([
reb('Recipient.documentDeletedAt', 'is', null),
reb('Recipient.signingStatus', '=', sql.lit(SigningStatus.NOT_SIGNED)),
reb('Recipient.role', '!=', sql.lit(RecipientRole.CC)),
]),
),
]),
);
return qb.where('Envelope.status', '!=', sql.lit(ExtendedDocumentStatus.DRAFT)).where((eb) =>
eb.and([
visibilityFilter(eb),
// Single EXISTS check: the team-email recipient must be NOT_SIGNED,
// non-CC, and not soft-deleted. Replaces teamDeletedFilter + separate
// recipientExists, eliminating a hashed SubPlan (~79k rows).
recipientExists(eb, teamEmail, (reb) =>
reb.and([
reb('Recipient.documentDeletedAt', 'is', null),
reb('Recipient.signingStatus', '=', sql.lit(SigningStatus.NOT_SIGNED)),
reb('Recipient.role', '!=', sql.lit(RecipientRole.CC)),
]),
),
]),
);
})
.with(ExtendedDocumentStatus.DRAFT, () =>
qb.where('Envelope.status', '=', sql.lit(ExtendedDocumentStatus.DRAFT)).where((eb) => {
@@ -466,10 +451,7 @@ export const findDocuments = async ({
const offset = Math.max(page - 1, 0) * perPage;
// Data query: paginated, executed directly via Kysely query builder
const dataQuery = filteredQuery
.orderBy(`Envelope.${orderByColumn}`, orderByDirection)
.limit(perPage)
.offset(offset);
const dataQuery = filteredQuery.orderBy(`Envelope.${orderByColumn}`, orderByDirection).limit(perPage).offset(offset);
// Count query: either windowed (fast, capped) or full (exact, for API consumers).
const baseCountQuery = filteredQuery.clearSelect().select('Envelope.id');
@@ -482,10 +464,7 @@ export const findDocuments = async ({
.selectFrom(baseCountQuery.as('filtered'))
.select(({ fn }) => fn.count<number>('id').as('total'));
const [dataResult, countResult] = await Promise.all([
dataQuery.execute(),
countQuery.executeTakeFirstOrThrow(),
]);
const [dataResult, countResult] = await Promise.all([dataQuery.execute(), countQuery.executeTakeFirstOrThrow()]);
const ids = dataResult.map((row) => row.id);
@@ -1,6 +1,5 @@
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { mapSecondaryIdToDocumentId } from '../../utils/envelope';
@@ -1,6 +1,5 @@
import { EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAuthMethods } from '../../types/document-auth';
@@ -7,9 +7,7 @@ export type GetDocumentCertificateAuditLogsOptions = {
envelopeId: string;
};
export const getDocumentCertificateAuditLogs = async ({
envelopeId,
}: GetDocumentCertificateAuditLogsOptions) => {
export const getDocumentCertificateAuditLogs = async ({ envelopeId }: GetDocumentCertificateAuditLogsOptions) => {
const rawAuditLogs = await prisma.documentAuditLog.findMany({
where: {
envelopeId,
@@ -9,11 +9,7 @@ export type GetDocumentWithDetailsByIdOptions = {
teamId: number;
};
export const getDocumentWithDetailsById = async ({
id,
userId,
teamId,
}: GetDocumentWithDetailsByIdOptions) => {
export const getDocumentWithDetailsById = async ({ id, userId, teamId }: GetDocumentWithDetailsByIdOptions) => {
const envelope = await getEnvelopeById({
id,
type: EnvelopeType.DOCUMENT,
@@ -4,9 +4,7 @@ export type GetRecipientOrSenderByShareLinkSlugOptions = {
slug: string;
};
export const getRecipientOrSenderByShareLinkSlug = async ({
slug,
}: GetRecipientOrSenderByShareLinkSlugOptions) => {
export const getRecipientOrSenderByShareLinkSlug = async ({ slug }: GetRecipientOrSenderByShareLinkSlugOptions) => {
const { envelopeId, email } = await prisma.documentShareLink.findFirstOrThrow({
where: {
slug,
+9 -33
View File
@@ -1,17 +1,10 @@
import {
DocumentStatus,
EnvelopeType,
RecipientRole,
SigningStatus,
TeamMemberRole,
} from '@prisma/client';
import type { Expression, ExpressionBuilder, SelectQueryBuilder, SqlBool } from 'kysely';
import { DateTime } from 'luxon';
import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
import { kyselyPrisma, prisma, sql } from '@documenso/prisma';
import type { DB } from '@documenso/prisma/generated/types';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { DocumentStatus, EnvelopeType, RecipientRole, SigningStatus, TeamMemberRole } from '@prisma/client';
import type { Expression, ExpressionBuilder, SelectQueryBuilder, SqlBool } from 'kysely';
import { DateTime } from 'luxon';
import { STATS_COUNT_CAP } from '../../constants/document';
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
@@ -87,14 +80,7 @@ const cappedCount = async (qb: EnvelopeQueryBuilder): Promise<number> => {
return Math.min(Number(result.total ?? 0), STATS_COUNT_CAP);
};
export const getStats = async ({
userId,
teamId,
period,
search = '',
folderId,
senderIds,
}: GetStatsInput) => {
export const getStats = async ({ userId, teamId, period, search = '', folderId, senderIds }: GetStatsInput) => {
const user = await prisma.user.findFirstOrThrow({
where: { id: userId },
select: { id: true, email: true },
@@ -113,18 +99,14 @@ export const getStats = async ({
// ─── Base query builder ──────────────────────────────────────────────
const buildBaseQuery = (): EnvelopeQueryBuilder => {
let qb: EnvelopeQueryBuilder = kyselyPrisma.$kysely
.selectFrom('Envelope')
.select('Envelope.id');
let qb: EnvelopeQueryBuilder = kyselyPrisma.$kysely.selectFrom('Envelope').select('Envelope.id');
// Type = DOCUMENT
qb = qb.where('Envelope.type', '=', sql.lit(EnvelopeType.DOCUMENT));
// Folder filter
qb =
folderId !== undefined
? qb.where('Envelope.folderId', '=', folderId)
: qb.where('Envelope.folderId', 'is', null);
folderId !== undefined ? qb.where('Envelope.folderId', '=', folderId) : qb.where('Envelope.folderId', 'is', null);
// Period filter
if (period) {
@@ -181,15 +163,11 @@ export const getStats = async ({
]);
const teamDeletedFilter = (eb: EnvelopeExpressionBuilder) => {
const branches = [
eb.and([eb('Envelope.teamId', '=', team.id), eb('Envelope.deletedAt', 'is', null)]),
];
const branches = [eb.and([eb('Envelope.teamId', '=', team.id), eb('Envelope.deletedAt', 'is', null)])];
if (teamEmail) {
branches.push(eb.and([senderEmailIs(eb, teamEmail), eb('Envelope.deletedAt', 'is', null)]));
branches.push(
recipientExists(eb, teamEmail, (reb) => reb('Recipient.documentDeletedAt', 'is', null)),
);
branches.push(recipientExists(eb, teamEmail, (reb) => reb('Recipient.documentDeletedAt', 'is', null)));
}
return eb.or(branches);
@@ -254,9 +232,7 @@ export const getStats = async ({
if (teamEmail) {
accessBranches.push(senderEmailIs(eb, teamEmail));
accessBranches.push(
recipientExists(eb, teamEmail, (reb) =>
reb('Recipient.signingStatus', '=', sql.lit(SigningStatus.REJECTED)),
),
recipientExists(eb, teamEmail, (reb) => reb('Recipient.signingStatus', '=', sql.lit(SigningStatus.REJECTED))),
);
}
@@ -1,19 +1,17 @@
import { prisma } from '@documenso/prisma';
import type { Envelope, Recipient } from '@prisma/client';
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import { validateTwoFactorTokenFromEmail } from '../2fa/email/validate-2fa-token-from-email';
import { verifyTwoFactorAuthenticationToken } from '../2fa/verify-2fa-token';
import { verifyPassword } from '../2fa/verify-password';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAuth, TDocumentAuthMethods } from '../../types/document-auth';
import { DocumentAuth } from '../../types/document-auth';
import type { TAuthenticationResponseJSONSchema } from '../../types/webauthn';
import { getAuthenticatorOptions } from '../../utils/authenticator';
import { extractDocumentAuthMethods } from '../../utils/document-auth';
import { validateTwoFactorTokenFromEmail } from '../2fa/email/validate-2fa-token-from-email';
import { verifyTwoFactorAuthenticationToken } from '../2fa/verify-2fa-token';
import { verifyPassword } from '../2fa/verify-password';
type IsRecipientAuthorizedOptions = {
// !: Probably find a better name than 'ACCESS_2FA' if requirements change.
@@ -71,10 +69,7 @@ export const isRecipientAuthorized = async ({
.exhaustive();
// Early true return when auth is not required.
if (
authMethods.length === 0 ||
authMethods.some((method) => method === DocumentAuth.EXPLICIT_NONE)
) {
if (authMethods.length === 0 || authMethods.some((method) => method === DocumentAuth.EXPLICIT_NONE)) {
return true;
}
@@ -1,7 +1,6 @@
import { DocumentStatus, EnvelopeType, SigningStatus } from '@prisma/client';
import { jobs } from '@documenso/lib/jobs/client';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, EnvelopeType, SigningStatus } from '@prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
@@ -18,12 +17,7 @@ export type RejectDocumentWithTokenOptions = {
requestMetadata?: RequestMetadata;
};
export async function rejectDocumentWithToken({
token,
id,
reason,
requestMetadata,
}: RejectDocumentWithTokenOptions) {
export async function rejectDocumentWithToken({ token, id, reason, requestMetadata }: RejectDocumentWithTokenOptions) {
// Find the recipient and document in a single query
const recipient = await prisma.recipient.findFirst({
where: {
@@ -1,5 +1,12 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
import { resolveExpiresAt } from '@documenso/lib/constants/envelope-expiration';
import { RECIPIENT_ROLE_TO_EMAIL_TYPE, RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import {
DocumentStatus,
@@ -9,27 +16,12 @@ import {
SigningStatus,
WebhookTriggerEvents,
} from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
import { resolveExpiresAt } from '@documenso/lib/constants/envelope-expiration';
import {
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '@documenso/lib/constants/recipient-roles';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
import { prisma } from '@documenso/prisma';
import { createElement } from 'react';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
import { isDocumentCompleted } from '../../utils/document';
import type { EnvelopeIdOptions } from '../../utils/envelope';
import { isRecipientEmailValidForSending } from '../../utils/recipients';
@@ -46,13 +38,7 @@ export type ResendDocumentOptions = {
requestMetadata: ApiRequestMetadata;
};
export const resendDocument = async ({
id,
userId,
recipients,
teamId,
requestMetadata,
}: ResendDocumentOptions) => {
export const resendDocument = async ({ id, userId, recipients, teamId, requestMetadata }: ResendDocumentOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
@@ -134,15 +120,14 @@ export const resendDocument = async ({
return envelope;
}
const { branding, emailLanguage, organisationType, senderEmail, replyToEmail } =
await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: envelope.teamId,
},
meta: envelope.documentMeta,
});
const { branding, emailLanguage, organisationType, senderEmail, replyToEmail } = await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: envelope.teamId,
},
meta: envelope.documentMeta,
});
await Promise.all(
recipientsToRemind.map(async (recipient) => {
@@ -157,9 +142,7 @@ export const resendDocument = async ({
const { email, name } = recipient;
const selfSigner = email === user.email;
const recipientActionVerb = i18n
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
.toLowerCase();
const recipientActionVerb = i18n._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb).toLowerCase();
let emailMessage = envelope.documentMeta.message || '';
let emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} this document`);
@@ -172,9 +155,7 @@ export const resendDocument = async ({
}
if (organisationType === OrganisationType.ORGANISATION) {
emailSubject = i18n._(
msg`Reminder: ${envelope.team.name} invited you to ${recipientActionVerb} a document`,
);
emailSubject = i18n._(msg`Reminder: ${envelope.team.name} invited you to ${recipientActionVerb} a document`);
emailMessage =
envelope.documentMeta.message ||
i18n._(
@@ -229,10 +210,7 @@ export const resendDocument = async ({
from: senderEmail,
replyTo: replyToEmail,
subject: envelope.documentMeta.subject
? renderCustomEmailTemplate(
i18n._(msg`Reminder: ${envelope.documentMeta.subject}`),
customEmailTemplate,
)
? renderCustomEmailTemplate(i18n._(msg`Reminder: ${envelope.documentMeta.subject}`), customEmailTemplate)
: emailSubject,
html,
text,
@@ -1,10 +1,9 @@
import { formatDocumentsPath, getHighestTeamRoleInGroup } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import type { Prisma } from '@prisma/client';
import { DocumentStatus, DocumentVisibility, EnvelopeType, TeamMemberRole } from '@prisma/client';
import { match } from 'ts-pattern';
import { formatDocumentsPath, getHighestTeamRoleInGroup } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { mapSecondaryIdToDocumentId } from '../../utils/envelope';
import { getUserTeamGroups } from '../team/get-user-team-groups';
@@ -14,11 +13,7 @@ export type SearchDocumentsWithKeywordOptions = {
limit?: number;
};
export const searchDocumentsWithKeyword = async ({
query,
userId,
limit = 20,
}: SearchDocumentsWithKeywordOptions) => {
export const searchDocumentsWithKeyword = async ({ query, userId, limit = 20 }: SearchDocumentsWithKeywordOptions) => {
if (!query.trim()) {
return [];
}
@@ -135,10 +130,7 @@ export const searchDocumentsWithKeyword = async ({
let path: string;
if (
envelope.userId === user.id ||
(envelope.teamId && teamGroupsByTeamId.has(envelope.teamId))
) {
if (envelope.userId === user.id || (envelope.teamId && teamGroupsByTeamId.has(envelope.teamId))) {
path = `${formatDocumentsPath(envelope.team.url)}/${legacyDocumentId}`;
} else {
const signingToken = envelope.recipients.find((r) => r.email === user.email)?.token;
@@ -1,11 +1,9 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { DocumentSource, EnvelopeType } from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { DocumentSource, EnvelopeType } from '@prisma/client';
import { createElement } from 'react';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
@@ -86,8 +84,7 @@ export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOp
const file = await getFileServerSide(envelopeItem.documentData);
// Use the envelope title for version 1, and the envelope item title for version 2.
const fileNameToUse =
envelope.internalVersion === 1 ? envelope.title : envelopeItem.title + '.pdf';
const fileNameToUse = envelope.internalVersion === 1 ? envelope.title : envelopeItem.title + '.pdf';
return {
filename: fileNameToUse.endsWith('.pdf') ? fileNameToUse : fileNameToUse + '.pdf',
@@ -104,9 +101,7 @@ export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOp
)}/${envelope.id}`;
if (envelope.team?.url) {
documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${envelope.team.url}/documents/${
envelope.id
}`;
documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${envelope.team.url}/documents/${envelope.id}`;
}
const emailSettings = extractDerivedDocumentEmailSettings(envelope.documentMeta);
@@ -120,8 +115,7 @@ export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOp
// - Recipient emails are disabled
if (
isOwnerDocumentCompletedEmailEnabled &&
(!envelope.recipients.find((recipient) => recipient.email === owner.email) ||
!isDocumentCompletedEmailEnabled)
(!envelope.recipients.find((recipient) => recipient.email === owner.email) || !isDocumentCompletedEmailEnabled)
) {
const template = createElement(DocumentCompletedEmailTemplate, {
documentName: envelope.title,
@@ -177,9 +171,7 @@ export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOp
return;
}
const recipientsToNotify = envelope.recipients.filter((recipient) =>
isRecipientEmailValidForSending(recipient),
);
const recipientsToNotify = envelope.recipients.filter((recipient) => isRecipientEmailValidForSending(recipient));
await Promise.all(
recipientsToNotify.map(async (recipient) => {
@@ -1,10 +1,8 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { mailer } from '@documenso/email/mailer';
import { DocumentSuperDeleteEmailTemplate } from '@documenso/email/templates/document-super-delete';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { createElement } from 'react';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
@@ -42,9 +40,7 @@ export const sendDeleteEmail = async ({ envelopeId, reason }: SendDeleteEmailOpt
});
}
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
envelope.documentMeta,
).documentDeleted;
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).documentDeleted;
if (!isDocumentDeletedEmailEnabled) {
return;
@@ -1,3 +1,9 @@
import { resolveExpiresAt } from '@documenso/lib/constants/envelope-expiration';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
import type { DocumentData, Envelope, EnvelopeItem, Field, Recipient } from '@prisma/client';
import {
DocumentSigningOrder,
@@ -10,13 +16,6 @@ import {
WebhookTriggerEvents,
} from '@prisma/client';
import { resolveExpiresAt } from '@documenso/lib/constants/envelope-expiration';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
import { validateCheckboxLength } from '../../advanced-fields-validation/validate-checkbox';
import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '../../constants/direct-templates';
import { AppError, AppErrorCode } from '../../errors/app-error';
@@ -30,20 +29,14 @@ import {
ZRadioFieldMeta,
ZTextFieldMeta,
} from '../../types/field-meta';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
import { getFileServerSide } from '../../universal/upload/get-file.server';
import { putNormalizedPdfFileServerSide } from '../../universal/upload/put-file.server';
import { isDocumentCompleted } from '../../utils/document';
import { extractDocumentAuthMethods } from '../../utils/document-auth';
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
import { toCheckboxCustomText, toRadioCustomText } from '../../utils/fields';
import {
getRecipientsWithMissingFields,
isRecipientEmailValidForSending,
} from '../../utils/recipients';
import { getRecipientsWithMissingFields, isRecipientEmailValidForSending } from '../../utils/recipients';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
@@ -56,13 +49,7 @@ export type SendDocumentOptions = {
requestMetadata: ApiRequestMetadata;
};
export const sendDocument = async ({
id,
userId,
teamId,
sendEmail,
requestMetadata,
}: SendDocumentOptions) => {
export const sendDocument = async ({ id, userId, teamId, sendEmail, requestMetadata }: SendDocumentOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id,
type: EnvelopeType.DOCUMENT,
@@ -156,10 +143,7 @@ export const sendDocument = async ({
});
// Validate that recipients who require fields (e.g., signers need signature fields) have them.
const recipientsWithMissingFields = getRecipientsWithMissingFields(
envelope.recipients,
envelope.fields,
);
const recipientsWithMissingFields = getRecipientsWithMissingFields(envelope.recipients, envelope.fields);
if (recipientsWithMissingFields.length > 0) {
const missingRecipientDescriptions = recipientsWithMissingFields
@@ -172,8 +156,7 @@ export const sendDocument = async ({
}
const allRecipientsHaveNoActionToTake = envelope.recipients.every(
(recipient) =>
recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
(recipient) => recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
);
if (allRecipientsHaveNoActionToTake) {
@@ -456,11 +439,7 @@ export const extractFieldAutoInsertValues = (
// Auto insert checkbox fields with the pre-checked values.
if (field.type === FieldType.CHECKBOX) {
const {
values = [],
validationRule,
validationLength,
} = ZCheckboxFieldMeta.parse(field.fieldMeta);
const { values = [], validationRule, validationLength } = ZCheckboxFieldMeta.parse(field.fieldMeta);
const checkedIndices: number[] = [];
@@ -1,11 +1,9 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { EnvelopeType } from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { EnvelopeType } from '@prisma/client';
import { createElement } from 'react';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
@@ -58,9 +56,7 @@ export const sendPendingEmail = async ({ id, recipientId }: SendPendingEmailOpti
meta: envelope.documentMeta,
});
const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings(
envelope.documentMeta,
).documentPending;
const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).documentPending;
if (!isDocumentPendingEmailEnabled) {
return;
@@ -1,15 +1,11 @@
import { EnvelopeType, ReadStatus, SendStatus, WebhookTriggerEvents } from '@prisma/client';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { EnvelopeType, ReadStatus, SendStatus, WebhookTriggerEvents } from '@prisma/client';
import type { TDocumentAccessAuthTypes } from '../../types/document-auth';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type ViewedDocumentOptions = {
@@ -18,11 +14,7 @@ export type ViewedDocumentOptions = {
requestMetadata?: RequestMetadata;
};
export const viewedDocument = async ({
token,
recipientAccessAuth,
requestMetadata,
}: ViewedDocumentOptions) => {
export const viewedDocument = async ({ token, recipientAccessAuth, requestMetadata }: ViewedDocumentOptions) => {
const recipient = await prisma.recipient.findFirst({
where: {
token,