mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 11:41:44 +10:00
fix: merge conflicts
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import { DOCUMENT_AUDIT_LOG_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 {
|
||||
@ -8,11 +9,15 @@ import {
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { jobs } from '../../jobs/client';
|
||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { sendPendingEmail } from './send-pending-email';
|
||||
@ -29,7 +34,7 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio
|
||||
return await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
@ -37,7 +42,7 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
@ -57,11 +62,11 @@ export const completeDocumentWithToken = async ({
|
||||
throw new Error(`Document ${document.id} must be pending`);
|
||||
}
|
||||
|
||||
if (document.Recipient.length === 0) {
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error(`Document ${document.id} has no recipient with token ${token}`);
|
||||
}
|
||||
|
||||
const [recipient] = document.Recipient;
|
||||
const [recipient] = document.recipients;
|
||||
|
||||
if (recipient.signingStatus === SigningStatus.SIGNED) {
|
||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||
@ -84,7 +89,7 @@ export const completeDocumentWithToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (fields.some((field) => !field.inserted)) {
|
||||
if (fieldsContainUnsignedRequiredField(fields)) {
|
||||
throw new Error(`Recipient ${recipient.id} has unsigned fields`);
|
||||
}
|
||||
|
||||
@ -138,6 +143,14 @@ export const completeDocumentWithToken = async ({
|
||||
});
|
||||
});
|
||||
|
||||
await jobs.triggerJob({
|
||||
name: 'send.recipient.signed.email',
|
||||
payload: {
|
||||
documentId: document.id,
|
||||
recipientId: recipient.id,
|
||||
},
|
||||
});
|
||||
|
||||
const pendingRecipients = await prisma.recipient.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
@ -185,7 +198,7 @@ export const completeDocumentWithToken = async ({
|
||||
const haveAllRecipientsSigned = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: document.id,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
every: {
|
||||
OR: [{ signingStatus: SigningStatus.SIGNED }, { role: RecipientRole.CC }],
|
||||
},
|
||||
@ -203,11 +216,19 @@ export const completeDocumentWithToken = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const updatedDocument = await getDocument({ token, documentId });
|
||||
const updatedDocument = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: document.id,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_SIGNED,
|
||||
data: updatedDocument,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: updatedDocument.userId,
|
||||
teamId: updatedDocument.teamId ?? undefined,
|
||||
});
|
||||
|
||||
248
packages/lib/server-only/document/create-document-v2.ts
Normal file
248
packages/lib/server-only/document/create-document-v2.ts
Normal file
@ -0,0 +1,248 @@
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentVisibility, TemplateMeta } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import type { TCreateDocumentV2Request } from '@documenso/trpc/server/document-router/schema';
|
||||
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import type { TDocumentFormValues } from '../../types/document-form-values';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putPdfFile } from '../../universal/upload/put-file';
|
||||
import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentDataId: string;
|
||||
normalizePdf?: boolean;
|
||||
data: {
|
||||
title: string;
|
||||
externalId?: string;
|
||||
visibility?: DocumentVisibility;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes;
|
||||
globalActionAuth?: TDocumentActionAuthTypes;
|
||||
formValues?: TDocumentFormValues;
|
||||
recipients: TCreateDocumentV2Request['recipients'];
|
||||
};
|
||||
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const createDocumentV2 = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentDataId,
|
||||
normalizePdf,
|
||||
data,
|
||||
meta,
|
||||
requestMetadata,
|
||||
}: CreateDocumentOptions) => {
|
||||
const { title, formValues } = data;
|
||||
|
||||
const team = teamId
|
||||
? await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
members: {
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
if (teamId !== undefined && !team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (normalizePdf) {
|
||||
const documentData = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
id: documentDataId,
|
||||
},
|
||||
});
|
||||
|
||||
if (documentData) {
|
||||
const buffer = await getFile(documentData);
|
||||
|
||||
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer));
|
||||
|
||||
const newDocumentData = await putPdfFile({
|
||||
name: title.endsWith('.pdf') ? title : `${title}.pdf`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(normalizedPdf),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
documentDataId = newDocumentData.id;
|
||||
}
|
||||
}
|
||||
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: data?.globalAccessAuth || null,
|
||||
globalActionAuth: data?.globalActionAuth || null,
|
||||
});
|
||||
|
||||
const recipientsHaveActionAuth = data.recipients?.some((recipient) => recipient.actionAuth);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (authOptions.globalActionAuth || recipientsHaveActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const visibility = determineDocumentVisibility(
|
||||
team?.teamGlobalSettings?.documentVisibility,
|
||||
team?.members[0].role ?? TeamMemberRole.MEMBER,
|
||||
);
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.create({
|
||||
data: {
|
||||
title,
|
||||
externalId: data.externalId,
|
||||
documentDataId,
|
||||
userId,
|
||||
teamId,
|
||||
authOptions,
|
||||
visibility,
|
||||
formValues,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
create: {
|
||||
...meta,
|
||||
signingOrder: meta?.signingOrder || undefined,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
language: meta?.language || team?.teamGlobalSettings?.documentLanguage,
|
||||
typedSignatureEnabled:
|
||||
meta?.typedSignatureEnabled ?? team?.teamGlobalSettings?.typedSignatureEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
(data.recipients || []).map(async (recipient) => {
|
||||
const recipientAuthOptions = createRecipientAuthOptions({
|
||||
accessAuth: recipient.accessAuth || null,
|
||||
actionAuth: recipient.actionAuth || null,
|
||||
});
|
||||
|
||||
await tx.recipient.create({
|
||||
data: {
|
||||
documentId: document.id,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
authOptions: recipientAuthOptions,
|
||||
fields: {
|
||||
createMany: {
|
||||
data: (recipient.fields || []).map((field) => ({
|
||||
documentId: document.id,
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Todo: Is it necessary to create a full audit log with all fields and recipients audit logs?
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
documentId: document.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
title,
|
||||
source: {
|
||||
type: DocumentSource.DOCUMENT,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const createdDocument = await tx.document.findFirst({
|
||||
where: {
|
||||
id: document.id,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdDocument) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return createdDocument;
|
||||
});
|
||||
};
|
||||
@ -1,14 +1,22 @@
|
||||
'use server';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
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 { DocumentSource, DocumentVisibility, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
import { DocumentSource, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putPdfFile } from '../../universal/upload/put-file';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentOptions = {
|
||||
@ -18,7 +26,9 @@ export type CreateDocumentOptions = {
|
||||
teamId?: number;
|
||||
documentDataId: string;
|
||||
formValues?: Record<string, string | number | boolean>;
|
||||
requestMetadata?: RequestMetadata;
|
||||
normalizePdf?: boolean;
|
||||
timezone?: string;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const createDocument = async ({
|
||||
@ -27,8 +37,10 @@ export const createDocument = async ({
|
||||
externalId,
|
||||
documentDataId,
|
||||
teamId,
|
||||
normalizePdf,
|
||||
formValues,
|
||||
requestMetadata,
|
||||
timezone,
|
||||
}: CreateDocumentOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
@ -47,7 +59,9 @@ export const createDocument = async ({
|
||||
teamId !== undefined &&
|
||||
!user.teamMembers.some((teamMember) => teamMember.teamId === teamId)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, 'Team not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
let team: (Team & { teamGlobalSettings: TeamGlobalSettings | null }) | null = null;
|
||||
@ -75,25 +89,28 @@ export const createDocument = async ({
|
||||
userTeamRole = teamWithUserRole.members[0]?.role;
|
||||
}
|
||||
|
||||
const determineVisibility = (
|
||||
globalVisibility: DocumentVisibility | null | undefined,
|
||||
userRole: TeamMemberRole,
|
||||
): DocumentVisibility => {
|
||||
const defaultVisibility = globalVisibility ?? DocumentVisibility.EVERYONE;
|
||||
if (normalizePdf) {
|
||||
const documentData = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
id: documentDataId,
|
||||
},
|
||||
});
|
||||
|
||||
if (userRole === TeamMemberRole.ADMIN) {
|
||||
return defaultVisibility;
|
||||
if (documentData) {
|
||||
const buffer = await getFile(documentData);
|
||||
|
||||
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer));
|
||||
|
||||
const newDocumentData = await putPdfFile({
|
||||
name: title.endsWith('.pdf') ? title : `${title}.pdf`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(normalizedPdf),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
documentDataId = newDocumentData.id;
|
||||
}
|
||||
|
||||
if (userRole === TeamMemberRole.MANAGER) {
|
||||
if (defaultVisibility === DocumentVisibility.ADMIN) {
|
||||
return DocumentVisibility.MANAGER_AND_ABOVE;
|
||||
}
|
||||
return defaultVisibility;
|
||||
}
|
||||
|
||||
return DocumentVisibility.EVERYONE;
|
||||
};
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.create({
|
||||
@ -103,7 +120,7 @@ export const createDocument = async ({
|
||||
documentDataId,
|
||||
userId,
|
||||
teamId,
|
||||
visibility: determineVisibility(
|
||||
visibility: determineDocumentVisibility(
|
||||
team?.teamGlobalSettings?.documentVisibility,
|
||||
userTeamRole ?? TeamMemberRole.MEMBER,
|
||||
),
|
||||
@ -112,6 +129,8 @@ export const createDocument = async ({
|
||||
documentMeta: {
|
||||
create: {
|
||||
language: team?.teamGlobalSettings?.documentLanguage,
|
||||
typedSignatureEnabled: team?.teamGlobalSettings?.typedSignatureEnabled,
|
||||
timezone: timezone,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -121,8 +140,7 @@ export const createDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
documentId: document.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
title,
|
||||
source: {
|
||||
@ -132,13 +150,27 @@ export const createDocument = async ({
|
||||
}),
|
||||
});
|
||||
|
||||
const createdDocument = await tx.document.findFirst({
|
||||
where: {
|
||||
id: document.id,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdDocument) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: document,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return document;
|
||||
return createdDocument;
|
||||
});
|
||||
};
|
||||
|
||||
@ -15,23 +15,29 @@ import type {
|
||||
TeamGlobalSettings,
|
||||
User,
|
||||
} from '@documenso/prisma/client';
|
||||
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, SendStatus, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type DeleteDocumentOptions = {
|
||||
id: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const deleteDocument = async ({
|
||||
@ -47,7 +53,9 @@ export const deleteDocument = async ({
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
const document = await prisma.document.findUnique({
|
||||
@ -55,7 +63,7 @@ export const deleteDocument = async ({
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
@ -67,15 +75,19 @@ export const deleteDocument = async ({
|
||||
});
|
||||
|
||||
if (!document || (teamId !== undefined && teamId !== document.teamId)) {
|
||||
throw new Error('Document not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isUserOwner = document.userId === userId;
|
||||
const isUserTeamMember = document.team?.members.some((member) => member.userId === userId);
|
||||
const userRecipient = document.Recipient.find((recipient) => recipient.email === user.email);
|
||||
const userRecipient = document.recipients.find((recipient) => recipient.email === user.email);
|
||||
|
||||
if (!isUserOwner && !isUserTeamMember && !userRecipient) {
|
||||
throw new Error('Not allowed');
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Not allowed',
|
||||
});
|
||||
}
|
||||
|
||||
// Handle hard or soft deleting the actual document if user has permission.
|
||||
@ -105,6 +117,13 @@ export const deleteDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CANCELLED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(document)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
// Return partial document for API v1 response.
|
||||
return {
|
||||
id: document.id,
|
||||
@ -121,7 +140,7 @@ export const deleteDocument = async ({
|
||||
|
||||
type HandleDocumentOwnerDeleteOptions = {
|
||||
document: Document & {
|
||||
Recipient: Recipient[];
|
||||
recipients: Recipient[];
|
||||
documentMeta: DocumentMeta | null;
|
||||
};
|
||||
team?:
|
||||
@ -130,7 +149,7 @@ type HandleDocumentOwnerDeleteOptions = {
|
||||
})
|
||||
| null;
|
||||
user: User;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
const handleDocumentOwnerDelete = async ({
|
||||
@ -150,8 +169,7 @@ const handleDocumentOwnerDelete = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId: document.id,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
type: 'SOFT',
|
||||
},
|
||||
@ -177,8 +195,7 @@ const handleDocumentOwnerDelete = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId: document.id,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
type: 'HARD',
|
||||
},
|
||||
@ -205,7 +222,7 @@ const handleDocumentOwnerDelete = async ({
|
||||
|
||||
// Send cancellation emails to recipients.
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
document.recipients.map(async (recipient) => {
|
||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,26 +1,27 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentSource, type Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export interface DuplicateDocumentByIdOptions {
|
||||
id: number;
|
||||
export interface DuplicateDocumentOptions {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
export const duplicateDocumentById = async ({
|
||||
id,
|
||||
export const duplicateDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: DuplicateDocumentByIdOptions) => {
|
||||
}: DuplicateDocumentOptions) => {
|
||||
const documentWhereInput = await getDocumentWhereInput({
|
||||
documentId: id,
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findUniqueOrThrow({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
select: {
|
||||
title: true,
|
||||
@ -45,10 +46,16 @@ export const duplicateDocumentById = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const createDocumentArguments: Prisma.DocumentCreateArgs = {
|
||||
data: {
|
||||
title: document.title,
|
||||
User: {
|
||||
user: {
|
||||
connect: {
|
||||
id: document.userId,
|
||||
},
|
||||
@ -78,5 +85,7 @@ export const duplicateDocumentById = async ({
|
||||
|
||||
const createdDocument = await prisma.document.create(createDocumentArguments);
|
||||
|
||||
return createdDocument.id;
|
||||
return {
|
||||
documentId: createdDocument.id,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentAuditLog } from '@documenso/prisma/client';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
import type { DocumentAuditLog, Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export interface FindDocumentAuditLogsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
@ -21,6 +22,7 @@ export interface FindDocumentAuditLogsOptions {
|
||||
|
||||
export const findDocumentAuditLogs = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
page = 1,
|
||||
perPage = 30,
|
||||
@ -31,26 +33,31 @@ export const findDocumentAuditLogs = async ({
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
|
||||
const documentFilter = await prisma.document.findFirstOrThrow({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const whereClause: Prisma.DocumentAuditLogWhereInput = {
|
||||
documentId,
|
||||
};
|
||||
@ -113,5 +120,5 @@ export const findDocumentAuditLogs = async ({
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
nextCursor,
|
||||
} satisfies FindResultSet<typeof parsedData> & { nextCursor?: string };
|
||||
} satisfies FindResultResponse<typeof parsedData> & { nextCursor?: string };
|
||||
};
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { P, match } from 'ts-pattern';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import type {
|
||||
Document,
|
||||
DocumentSource,
|
||||
@ -11,10 +10,11 @@ import type {
|
||||
TeamEmail,
|
||||
User,
|
||||
} from '@documenso/prisma/client';
|
||||
import { RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import type { FindResultSet } from '../../types/find-result-set';
|
||||
import { type FindResultResponse } from '../../types/search-params';
|
||||
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
|
||||
|
||||
export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
|
||||
@ -22,7 +22,6 @@ export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
|
||||
export type FindDocumentsOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
term?: string;
|
||||
templateId?: number;
|
||||
source?: DocumentSource;
|
||||
status?: ExtendedDocumentStatus;
|
||||
@ -34,13 +33,12 @@ export type FindDocumentsOptions = {
|
||||
};
|
||||
period?: PeriodSelectorValue;
|
||||
senderIds?: number[];
|
||||
search?: string;
|
||||
query?: string;
|
||||
};
|
||||
|
||||
export const findDocuments = async ({
|
||||
userId,
|
||||
teamId,
|
||||
term,
|
||||
templateId,
|
||||
source,
|
||||
status = ExtendedDocumentStatus.ALL,
|
||||
@ -49,7 +47,7 @@ export const findDocuments = async ({
|
||||
orderBy,
|
||||
period,
|
||||
senderIds,
|
||||
search,
|
||||
query,
|
||||
}: FindDocumentsOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
@ -87,22 +85,11 @@ export const findDocuments = async ({
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
const teamMemberRole = team?.members[0].role ?? null;
|
||||
|
||||
const termFilters = match(term)
|
||||
.with(P.string.minLength(1), () => {
|
||||
return {
|
||||
title: {
|
||||
contains: term,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
} as const;
|
||||
})
|
||||
.otherwise(() => undefined);
|
||||
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ Recipient: { some: { name: { contains: search, mode: 'insensitive' } } } },
|
||||
{ Recipient: { some: { email: { contains: search, mode: 'insensitive' } } } },
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
||||
],
|
||||
};
|
||||
|
||||
@ -126,7 +113,7 @@ export const findDocuments = async ({
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
@ -163,7 +150,7 @@ export const findDocuments = async ({
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
documentDeletedAt: null,
|
||||
@ -184,13 +171,13 @@ export const findDocuments = async ({
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
User: {
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: team.teamEmail.email,
|
||||
documentDeletedAt: null,
|
||||
@ -209,7 +196,6 @@ export const findDocuments = async ({
|
||||
}
|
||||
|
||||
const whereAndClause: Prisma.DocumentWhereInput['AND'] = [
|
||||
{ ...termFilters },
|
||||
{ ...filters },
|
||||
{ ...deletedFilter },
|
||||
{ ...searchFilter },
|
||||
@ -256,14 +242,14 @@ export const findDocuments = async ({
|
||||
[orderByColumn]: orderByDirection,
|
||||
},
|
||||
include: {
|
||||
User: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
@ -290,7 +276,7 @@ export const findDocuments = async ({
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultSet<typeof data>;
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
|
||||
const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
@ -303,7 +289,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
},
|
||||
{
|
||||
status: ExtendedDocumentStatus.COMPLETED,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
@ -311,7 +297,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
},
|
||||
{
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
@ -323,7 +309,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
status: {
|
||||
not: ExtendedDocumentStatus.DRAFT,
|
||||
},
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
@ -347,7 +333,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
},
|
||||
{
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
@ -368,7 +354,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
},
|
||||
{
|
||||
status: ExtendedDocumentStatus.COMPLETED,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
@ -433,7 +419,7 @@ const findTeamDocumentsFilter = (
|
||||
status: {
|
||||
not: ExtendedDocumentStatus.DRAFT,
|
||||
},
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
},
|
||||
@ -443,7 +429,7 @@ const findTeamDocumentsFilter = (
|
||||
|
||||
// Filter to display all documents that have been sent by the team email.
|
||||
filter.OR.push({
|
||||
User: {
|
||||
user: {
|
||||
email: teamEmail,
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
@ -462,7 +448,7 @@ const findTeamDocumentsFilter = (
|
||||
status: {
|
||||
not: ExtendedDocumentStatus.DRAFT,
|
||||
},
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
@ -488,7 +474,7 @@ const findTeamDocumentsFilter = (
|
||||
if (teamEmail && filter.OR) {
|
||||
filter.OR.push({
|
||||
status: ExtendedDocumentStatus.DRAFT,
|
||||
User: {
|
||||
user: {
|
||||
email: teamEmail,
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
@ -513,7 +499,7 @@ const findTeamDocumentsFilter = (
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
OR: [
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
@ -525,7 +511,7 @@ const findTeamDocumentsFilter = (
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
{
|
||||
User: {
|
||||
user: {
|
||||
email: teamEmail,
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
@ -550,7 +536,7 @@ const findTeamDocumentsFilter = (
|
||||
if (teamEmail && filter.OR) {
|
||||
filter.OR.push(
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
},
|
||||
@ -558,7 +544,7 @@ const findTeamDocumentsFilter = (
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
{
|
||||
User: {
|
||||
user: {
|
||||
email: teamEmail,
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
|
||||
@ -4,35 +4,36 @@ import { prisma } from '@documenso/prisma';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type GetDocumentByIdOptions = {
|
||||
id: number;
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const getDocumentById = async ({ id, userId, teamId }: GetDocumentByIdOptions) => {
|
||||
export const getDocumentById = async ({ documentId, userId, teamId }: GetDocumentByIdOptions) => {
|
||||
const documentWhereInput = await getDocumentWhereInput({
|
||||
documentId: id,
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return await prisma.document.findFirstOrThrow({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
User: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
Recipient: {
|
||||
recipients: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
@ -45,6 +46,14 @@ export const getDocumentById = async ({ id, userId, teamId }: GetDocumentByIdOpt
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document could not be found',
|
||||
});
|
||||
}
|
||||
|
||||
return document;
|
||||
};
|
||||
|
||||
export type GetDocumentWhereInputOptions = {
|
||||
@ -110,14 +119,14 @@ export const getDocumentWhereInput = async ({
|
||||
if (team.teamEmail) {
|
||||
documentWhereInput.OR.push(
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
User: {
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
@ -145,7 +154,7 @@ export const getDocumentWhereInput = async ({
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
|
||||
@ -41,7 +41,7 @@ export const getDocumentByToken = async ({ token }: GetDocumentByTokenOptions) =
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
@ -66,28 +66,39 @@ export const getDocumentAndSenderByToken = async ({
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
User: true,
|
||||
user: true,
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
teamEmail: true,
|
||||
teamGlobalSettings: {
|
||||
select: {
|
||||
includeSenderDetails: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
const { password: _password, ...User } = result.User;
|
||||
const { password: _password, ...user } = result.user;
|
||||
|
||||
const recipient = result.Recipient[0];
|
||||
const recipient = result.recipients[0];
|
||||
|
||||
// Sanity check, should not be possible.
|
||||
if (!recipient) {
|
||||
@ -107,12 +118,14 @@ export const getDocumentAndSenderByToken = async ({
|
||||
}
|
||||
|
||||
if (!documentAccessValid) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid access values');
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Invalid access values',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
User,
|
||||
user,
|
||||
};
|
||||
};
|
||||
|
||||
@ -131,14 +144,14 @@ export const getDocumentAndRecipientByToken = async ({
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
@ -147,7 +160,7 @@ export const getDocumentAndRecipientByToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const [recipient] = result.Recipient;
|
||||
const [recipient] = result.recipients;
|
||||
|
||||
// Sanity check, should not be possible.
|
||||
if (!recipient) {
|
||||
@ -167,11 +180,10 @@ export const getDocumentAndRecipientByToken = async ({
|
||||
}
|
||||
|
||||
if (!documentAccessValid) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid access values');
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Invalid access values',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
Recipient: result.Recipient,
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
@ -1,32 +1,40 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentWithDetails } from '@documenso/prisma/types/document';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export type GetDocumentWithDetailsByIdOptions = {
|
||||
id: number;
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const getDocumentWithDetailsById = async ({
|
||||
id,
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetDocumentWithDetailsByIdOptions): Promise<DocumentWithDetails> => {
|
||||
}: GetDocumentWithDetailsByIdOptions) => {
|
||||
const documentWhereInput = await getDocumentWhereInput({
|
||||
documentId: id,
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return await prisma.document.findFirstOrThrow({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
Field: true,
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
return document;
|
||||
};
|
||||
|
||||
@ -85,8 +85,8 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => {
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ Recipient: { some: { name: { contains: search, mode: 'insensitive' } } } },
|
||||
{ Recipient: { some: { email: { contains: search, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { name: { contains: search, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { email: { contains: search, mode: 'insensitive' } } } },
|
||||
],
|
||||
};
|
||||
|
||||
@ -113,7 +113,7 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => {
|
||||
},
|
||||
where: {
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
@ -132,7 +132,7 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => {
|
||||
},
|
||||
where: {
|
||||
createdAt,
|
||||
User: {
|
||||
user: {
|
||||
email: {
|
||||
not: user.email,
|
||||
},
|
||||
@ -140,7 +140,7 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => {
|
||||
OR: [
|
||||
{
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
@ -150,7 +150,7 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => {
|
||||
},
|
||||
{
|
||||
status: ExtendedDocumentStatus.COMPLETED,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
@ -191,8 +191,8 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: options.search, mode: 'insensitive' } },
|
||||
{ Recipient: { some: { name: { contains: options.search, mode: 'insensitive' } } } },
|
||||
{ Recipient: { some: { email: { contains: options.search, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { name: { contains: options.search, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { email: { contains: options.search, mode: 'insensitive' } } } },
|
||||
],
|
||||
};
|
||||
|
||||
@ -234,7 +234,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
{
|
||||
OR: [
|
||||
{ userId: options.userId },
|
||||
{ Recipient: { some: { email: options.currentUserEmail } } },
|
||||
{ recipients: { some: { email: options.currentUserEmail } } },
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -257,7 +257,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
teamId,
|
||||
},
|
||||
{
|
||||
User: {
|
||||
user: {
|
||||
email: teamEmail,
|
||||
},
|
||||
},
|
||||
@ -274,7 +274,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
userId: userIdWhereClause,
|
||||
createdAt,
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
@ -296,7 +296,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
OR: [
|
||||
{
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
@ -307,7 +307,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
},
|
||||
{
|
||||
status: ExtendedDocumentStatus.COMPLETED,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
|
||||
@ -106,7 +106,9 @@ export const isRecipientAuthorized = async ({
|
||||
|
||||
// Should not be possible.
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, 'User not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
return await verifyTwoFactorAuthenticationToken({
|
||||
@ -164,7 +166,9 @@ const verifyPasskey = async ({
|
||||
});
|
||||
|
||||
if (!passkey) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, 'Passkey not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Passkey not found',
|
||||
});
|
||||
}
|
||||
|
||||
const verificationToken = await prisma.verificationToken
|
||||
@ -177,11 +181,15 @@ const verifyPasskey = async ({
|
||||
.catch(() => null);
|
||||
|
||||
if (!verificationToken) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, 'Token not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Token not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationToken.expires < new Date()) {
|
||||
throw new AppError(AppErrorCode.EXPIRED_CODE, 'Token expired');
|
||||
throw new AppError(AppErrorCode.EXPIRED_CODE, {
|
||||
message: 'Token expired',
|
||||
});
|
||||
}
|
||||
|
||||
const { rpId, origin } = getAuthenticatorOptions();
|
||||
@ -199,7 +207,9 @@ const verifyPasskey = async ({
|
||||
}).catch(() => null); // May want to log this for insights.
|
||||
|
||||
if (verification?.verified !== true) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, 'User is not authorized');
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'User is not authorized',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.passkey.update({
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
@ -10,7 +9,7 @@ export type MoveDocumentToTeamOptions = {
|
||||
documentId: number;
|
||||
teamId: number;
|
||||
userId: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const moveDocumentToTeam = async ({
|
||||
@ -20,10 +19,6 @@ export const moveDocumentToTeam = async ({
|
||||
requestMetadata,
|
||||
}: MoveDocumentToTeamOptions) => {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.findUniqueOrThrow({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
const document = await tx.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
@ -33,8 +28,7 @@ export const moveDocumentToTeam = async ({
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found or already associated with a team.',
|
||||
});
|
||||
}
|
||||
@ -51,9 +45,8 @@ export const moveDocumentToTeam = async ({
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'You are not a member of this team.',
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'This team does not exist, or you are not a member of this team.',
|
||||
});
|
||||
}
|
||||
|
||||
@ -62,12 +55,11 @@ export const moveDocumentToTeam = async ({
|
||||
data: { teamId },
|
||||
});
|
||||
|
||||
const log = await tx.documentAuditLog.create({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
|
||||
documentId: updatedDocument.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
movedByUserId: userId,
|
||||
fromPersonalAccount: true,
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { jobs } from '@documenso/lib/jobs/client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type RejectDocumentWithTokenOptions = {
|
||||
token: string;
|
||||
@ -28,25 +34,24 @@ export async function rejectDocumentWithToken({
|
||||
documentId,
|
||||
},
|
||||
include: {
|
||||
Document: {
|
||||
document: {
|
||||
include: {
|
||||
User: true,
|
||||
user: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const document = recipient?.Document;
|
||||
const document = recipient?.document;
|
||||
|
||||
if (!recipient || !document) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document or recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Add the audit log entry before updating the recipient
|
||||
|
||||
// Update the recipient status to rejected
|
||||
const [updatedRecipient] = await prisma.$transaction([
|
||||
prisma.recipient.update({
|
||||
@ -88,5 +93,28 @@ export async function rejectDocumentWithToken({
|
||||
},
|
||||
});
|
||||
|
||||
// Get the updated document with all recipients
|
||||
const updatedDocument = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: document.id,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!updatedDocument) {
|
||||
throw new Error('Document not found after update');
|
||||
}
|
||||
|
||||
// Trigger webhook for document rejection
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_REJECTED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
});
|
||||
|
||||
return updatedRecipient;
|
||||
}
|
||||
|
||||
@ -10,12 +10,12 @@ import {
|
||||
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 { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
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 { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
@ -29,7 +29,7 @@ export type ResendDocumentOptions = {
|
||||
userId: number;
|
||||
recipients: number[];
|
||||
teamId?: number;
|
||||
requestMetadata: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const resendDocument = async ({
|
||||
@ -38,7 +38,7 @@ export const resendDocument = async ({
|
||||
recipients,
|
||||
teamId,
|
||||
requestMetadata,
|
||||
}: ResendDocumentOptions) => {
|
||||
}: ResendDocumentOptions): Promise<void> => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
@ -54,7 +54,7 @@ export const resendDocument = async ({
|
||||
const document = await prisma.document.findUnique({
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: {
|
||||
in: recipients,
|
||||
@ -80,7 +80,7 @@ export const resendDocument = async ({
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.Recipient.length === 0) {
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ export const resendDocument = async ({
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
document.recipients.map(async (recipient) => {
|
||||
if (recipient.role === RecipientRole.CC) {
|
||||
return;
|
||||
}
|
||||
@ -134,7 +134,7 @@ export const resendDocument = async ({
|
||||
emailMessage =
|
||||
customEmail?.message ||
|
||||
i18n._(
|
||||
msg`${user.name} on behalf of ${document.team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
|
||||
msg`${user.name} on behalf of "${document.team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -201,8 +201,7 @@ export const resendDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
emailType: recipientEmailType,
|
||||
recipientEmail: recipient.email,
|
||||
|
||||
@ -6,14 +6,22 @@ import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-po
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentStatus,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import { ZSupportedLanguageCodeSchema } from '../../constants/i18n';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putPdfFile } from '../../universal/upload/put-file';
|
||||
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
||||
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
||||
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
||||
import { flattenForm } from '../pdf/flatten-form';
|
||||
@ -38,7 +46,7 @@ export const sealDocument = async ({
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
every: {
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
},
|
||||
@ -47,7 +55,16 @@ export const sealDocument = async ({
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
teamGlobalSettings: {
|
||||
select: {
|
||||
includeSigningCertificate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -75,12 +92,12 @@ export const sealDocument = async ({
|
||||
documentId: document.id,
|
||||
},
|
||||
include: {
|
||||
Signature: true,
|
||||
signature: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (fields.some((field) => !field.inserted)) {
|
||||
throw new Error(`Document ${document.id} has unsigned fields`);
|
||||
if (fieldsContainUnsignedRequiredField(fields)) {
|
||||
throw new Error(`Document ${document.id} has unsigned required fields`);
|
||||
}
|
||||
|
||||
if (isResealing) {
|
||||
@ -92,11 +109,13 @@ export const sealDocument = async ({
|
||||
// !: Need to write the fields onto the document as a hard copy
|
||||
const pdfData = await getFile(documentData);
|
||||
|
||||
const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
|
||||
|
||||
const certificate = await getCertificatePdf({ documentId, language: documentLanguage })
|
||||
.then(async (doc) => PDFDocument.load(doc))
|
||||
.catch(() => null);
|
||||
const certificateData =
|
||||
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
|
||||
const doc = await PDFDocument.load(pdfData);
|
||||
|
||||
@ -105,7 +124,9 @@ export const sealDocument = async ({
|
||||
flattenForm(doc);
|
||||
flattenAnnotations(doc);
|
||||
|
||||
if (certificate) {
|
||||
if (certificateData) {
|
||||
const certificate = await PDFDocument.load(certificateData);
|
||||
|
||||
const certificatePages = await doc.copyPages(certificate, certificate.getPageIndices());
|
||||
|
||||
certificatePages.forEach((page) => {
|
||||
@ -124,10 +145,10 @@ export const sealDocument = async ({
|
||||
|
||||
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
|
||||
|
||||
const { name, ext } = path.parse(document.title);
|
||||
const { name } = path.parse(document.title);
|
||||
|
||||
const { data: newData } = await putPdfFile({
|
||||
name: `${name}_signed${ext}`,
|
||||
name: `${name}_signed.pdf`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
||||
});
|
||||
@ -187,13 +208,14 @@ export const sealDocument = async ({
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
Recipient: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
||||
data: updatedDocument,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
});
|
||||
|
||||
@ -35,7 +35,7 @@ export const searchDocumentsWithKeyword = async ({
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: {
|
||||
contains: query,
|
||||
@ -48,7 +48,7 @@ export const searchDocumentsWithKeyword = async ({
|
||||
},
|
||||
{
|
||||
status: DocumentStatus.COMPLETED,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
@ -60,7 +60,7 @@ export const searchDocumentsWithKeyword = async ({
|
||||
},
|
||||
{
|
||||
status: DocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
@ -91,7 +91,7 @@ export const searchDocumentsWithKeyword = async ({
|
||||
],
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
url: true,
|
||||
@ -140,7 +140,7 @@ export const searchDocumentsWithKeyword = async ({
|
||||
return canAccessDocument;
|
||||
})
|
||||
.map((document) => {
|
||||
const { Recipient, ...documentWithoutRecipient } = document;
|
||||
const { recipients, ...documentWithoutRecipient } = document;
|
||||
|
||||
let documentPath;
|
||||
|
||||
@ -149,13 +149,13 @@ export const searchDocumentsWithKeyword = async ({
|
||||
} else if (document.teamId && document.team) {
|
||||
documentPath = `${formatDocumentsPath(document.team.url)}/${document.id}`;
|
||||
} else {
|
||||
documentPath = getSigningLink(Recipient, user);
|
||||
documentPath = getSigningLink(recipients, user);
|
||||
}
|
||||
|
||||
return {
|
||||
...documentWithoutRecipient,
|
||||
path: documentPath,
|
||||
value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '),
|
||||
value: [document.id, document.title, ...document.recipients.map((r) => r.email)].join(' '),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } 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 { prisma } from '@documenso/prisma';
|
||||
@ -13,7 +13,7 @@ export type SelfSignDocumentOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata?: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const selfSignDocument = async ({
|
||||
@ -53,7 +53,7 @@ export const selfSignDocument = async ({
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
||||
},
|
||||
documentMeta: true,
|
||||
@ -65,11 +65,11 @@ export const selfSignDocument = async ({
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.Recipient.length === 0) {
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
if (document.Recipient.length !== 1 || document.Recipient[0].email !== user.email) {
|
||||
if (document.recipients.length !== 1 || document.recipients[0].email !== user.email) {
|
||||
throw new Error('Invalid document for self-signing');
|
||||
}
|
||||
|
||||
@ -111,15 +111,15 @@ export const selfSignDocument = async ({
|
||||
}
|
||||
|
||||
const recipientHasNoActionToTake =
|
||||
document.Recipient[0].role === RecipientRole.CC ||
|
||||
document.Recipient[0].signingStatus === SigningStatus.SIGNED;
|
||||
document.recipients[0].role === RecipientRole.CC ||
|
||||
document.recipients[0].signingStatus === SigningStatus.SIGNED;
|
||||
|
||||
if (recipientHasNoActionToTake) {
|
||||
await jobs.triggerJob({
|
||||
name: 'internal.seal-document',
|
||||
payload: {
|
||||
documentId,
|
||||
requestMetadata,
|
||||
requestMetadata: requestMetadata?.requestMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
@ -128,7 +128,7 @@ export const selfSignDocument = async ({
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -139,13 +139,13 @@ export const selfSignDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.SELF_SIGN,
|
||||
documentId: document.id,
|
||||
requestMetadata,
|
||||
requestMetadata: requestMetadata?.requestMetadata,
|
||||
user,
|
||||
data: {
|
||||
recipientId: document.Recipient[0].id,
|
||||
recipientEmail: document.Recipient[0].email,
|
||||
recipientName: document.Recipient[0].name,
|
||||
recipientRole: document.Recipient[0].role,
|
||||
recipientId: document.recipients[0].id,
|
||||
recipientEmail: document.recipients[0].email,
|
||||
recipientName: document.recipients[0].name,
|
||||
recipientRole: document.recipients[0].role,
|
||||
},
|
||||
}),
|
||||
});
|
||||
@ -153,7 +153,7 @@ export const selfSignDocument = async ({
|
||||
|
||||
await tx.recipient.update({
|
||||
where: {
|
||||
id: document.Recipient[0].id,
|
||||
id: document.recipients[0].id,
|
||||
},
|
||||
data: {
|
||||
sendStatus: SendStatus.SENT,
|
||||
@ -168,7 +168,7 @@ export const selfSignDocument = async ({
|
||||
status: DocumentStatus.PENDING,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -32,8 +32,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
User: true,
|
||||
recipients: true,
|
||||
user: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
@ -50,11 +50,11 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
|
||||
const isDirectTemplate = document?.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
||||
|
||||
if (document.Recipient.length === 0) {
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const { User: owner } = document;
|
||||
const { user: owner } = document;
|
||||
|
||||
const completedDocument = await getFile(document.documentData);
|
||||
|
||||
@ -72,14 +72,19 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
const isDocumentCompletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).documentCompleted;
|
||||
const emailSettings = extractDerivedDocumentEmailSettings(document.documentMeta);
|
||||
const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted;
|
||||
const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted;
|
||||
|
||||
// If the document owner is not a recipient, OR recipient emails are disabled, then send the email to them separately.
|
||||
// Send email to document owner if:
|
||||
// 1. Owner document completed emails are enabled AND
|
||||
// 2. Either:
|
||||
// - The owner is not a recipient, OR
|
||||
// - Recipient emails are disabled
|
||||
if (
|
||||
!document.Recipient.find((recipient) => recipient.email === owner.email) ||
|
||||
!isDocumentCompletedEmailEnabled
|
||||
isOwnerDocumentCompletedEmailEnabled &&
|
||||
(!document.recipients.find((recipient) => recipient.email === owner.email) ||
|
||||
!isDocumentCompletedEmailEnabled)
|
||||
) {
|
||||
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
@ -145,7 +150,7 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
document.recipients.map(async (recipient) => {
|
||||
const customEmailTemplate = {
|
||||
'signer.name': recipient.name,
|
||||
'signer.email': recipient.email,
|
||||
|
||||
@ -8,6 +8,7 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
@ -23,7 +24,7 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
User: true,
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
@ -34,7 +35,9 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
@ -45,7 +48,7 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
return;
|
||||
}
|
||||
|
||||
const { email, name } = document.User;
|
||||
const { email, name } = document.user;
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } 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 { prisma } from '@documenso/prisma';
|
||||
@ -9,11 +9,15 @@ import {
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { jobs } from '../../jobs/client';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
@ -23,7 +27,7 @@ export type SendDocumentOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
sendEmail?: boolean;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const sendDocument = async ({
|
||||
@ -33,17 +37,6 @@ export const sendDocument = async ({
|
||||
sendEmail,
|
||||
requestMetadata,
|
||||
}: SendDocumentOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
const document = await prisma.document.findUnique({
|
||||
where: {
|
||||
id: documentId,
|
||||
@ -64,7 +57,7 @@ export const sendDocument = async ({
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
||||
},
|
||||
documentMeta: true,
|
||||
@ -76,7 +69,7 @@ export const sendDocument = async ({
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.Recipient.length === 0) {
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
@ -86,13 +79,13 @@ export const sendDocument = async ({
|
||||
|
||||
const signingOrder = document.documentMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
|
||||
|
||||
let recipientsToNotify = document.Recipient;
|
||||
let recipientsToNotify = document.recipients;
|
||||
|
||||
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
// Get the currently active recipient.
|
||||
recipientsToNotify = document.Recipient.filter(
|
||||
(r) => r.signingStatus === SigningStatus.NOT_SIGNED && r.role !== RecipientRole.CC,
|
||||
).slice(0, 1);
|
||||
recipientsToNotify = document.recipients
|
||||
.filter((r) => r.signingStatus === SigningStatus.NOT_SIGNED && r.role !== RecipientRole.CC)
|
||||
.slice(0, 1);
|
||||
|
||||
// Secondary filter so we aren't resending if the current active recipient has already
|
||||
// received the document.
|
||||
@ -114,8 +107,14 @@ export const sendDocument = async ({
|
||||
formValues: document.formValues as Record<string, string | number | boolean>,
|
||||
});
|
||||
|
||||
let fileName = document.title;
|
||||
|
||||
if (!document.title.endsWith('.pdf')) {
|
||||
fileName = `${document.title}.pdf`;
|
||||
}
|
||||
|
||||
const newDocumentData = await putPdfFile({
|
||||
name: document.title,
|
||||
name: fileName,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(prefilled),
|
||||
});
|
||||
@ -177,14 +176,14 @@ export const sendDocument = async ({
|
||||
userId,
|
||||
documentId,
|
||||
recipientId: recipient.id,
|
||||
requestMetadata,
|
||||
requestMetadata: requestMetadata?.requestMetadata,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const allRecipientsHaveNoActionToTake = document.Recipient.every(
|
||||
const allRecipientsHaveNoActionToTake = document.recipients.every(
|
||||
(recipient) =>
|
||||
recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
|
||||
);
|
||||
@ -194,7 +193,7 @@ export const sendDocument = async ({
|
||||
name: 'internal.seal-document',
|
||||
payload: {
|
||||
documentId,
|
||||
requestMetadata,
|
||||
requestMetadata: requestMetadata?.requestMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
@ -204,7 +203,8 @@ export const sendDocument = async ({
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -215,8 +215,7 @@ export const sendDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
|
||||
documentId: document.id,
|
||||
requestMetadata,
|
||||
user,
|
||||
metadata: requestMetadata,
|
||||
data: {},
|
||||
}),
|
||||
});
|
||||
@ -230,14 +229,15 @@ export const sendDocument = async ({
|
||||
status: DocumentStatus.PENDING,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_SENT,
|
||||
data: updatedDocument,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
@ -21,14 +21,14 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
@ -46,7 +46,7 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.Recipient.length === 0) {
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
return;
|
||||
}
|
||||
|
||||
const [recipient] = document.Recipient;
|
||||
const [recipient] = document.recipients;
|
||||
|
||||
const { email, name } = recipient;
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
@ -30,9 +31,9 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
User: true,
|
||||
user: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
@ -42,10 +43,12 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const { status, User: user } = document;
|
||||
const { status, user } = document;
|
||||
|
||||
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
@ -54,11 +57,11 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
// if the document is pending, send cancellation emails to all recipients
|
||||
if (
|
||||
status === DocumentStatus.PENDING &&
|
||||
document.Recipient.length > 0 &&
|
||||
document.recipients.length > 0 &&
|
||||
isDocumentDeletedEmailEnabled
|
||||
) {
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
document.recipients.map(async (recipient) => {
|
||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,274 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
|
||||
export type UpdateDocumentSettingsOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
data: {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||
};
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const updateDocumentSettings = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
data,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentSettingsOptions) => {
|
||||
if (!data.title && !data.globalAccessAuth && !data.globalActionAuth) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, 'Missing data to update');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (teamId) {
|
||||
const currentUserRole = document.team?.members[0]?.role;
|
||||
|
||||
match(currentUserRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => {
|
||||
const allowedVisibilities: DocumentVisibility[] = [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
];
|
||||
|
||||
if (
|
||||
!allowedVisibilities.includes(document.visibility) ||
|
||||
(data.visibility && !allowedVisibilities.includes(data.visibility))
|
||||
) {
|
||||
throw new AppError(
|
||||
AppErrorCode.UNAUTHORIZED,
|
||||
'You do not have permission to update the document visibility',
|
||||
);
|
||||
}
|
||||
})
|
||||
.with(TeamMemberRole.MEMBER, () => {
|
||||
if (
|
||||
document.visibility !== DocumentVisibility.EVERYONE ||
|
||||
(data.visibility && data.visibility !== DocumentVisibility.EVERYONE)
|
||||
) {
|
||||
throw new AppError(
|
||||
AppErrorCode.UNAUTHORIZED,
|
||||
'You do not have permission to update the document visibility',
|
||||
);
|
||||
}
|
||||
})
|
||||
.otherwise(() => {
|
||||
throw new AppError(
|
||||
AppErrorCode.UNAUTHORIZED,
|
||||
'You do not have permission to update the document',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
});
|
||||
|
||||
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
|
||||
const documentGlobalActionAuth = documentAuthOption?.globalActionAuth ?? null;
|
||||
|
||||
// If the new global auth values aren't passed in, fallback to the current document values.
|
||||
const newGlobalAccessAuth =
|
||||
data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth;
|
||||
const newGlobalActionAuth =
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(
|
||||
AppErrorCode.UNAUTHORIZED,
|
||||
'You do not have permission to set the action auth',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const isTitleSame = data.title === undefined || data.title === document.title;
|
||||
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
|
||||
const isGlobalAccessSame =
|
||||
documentGlobalAccessAuth === undefined || documentGlobalAccessAuth === newGlobalAccessAuth;
|
||||
const isGlobalActionSame =
|
||||
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
||||
const isDocumentVisibilitySame =
|
||||
data.visibility === undefined || data.visibility === document.visibility;
|
||||
|
||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||
|
||||
if (!isTitleSame && document.status !== DocumentStatus.DRAFT) {
|
||||
throw new AppError(
|
||||
AppErrorCode.INVALID_BODY,
|
||||
'You cannot update the title if the document has been sent',
|
||||
);
|
||||
}
|
||||
|
||||
if (!isTitleSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: document.title,
|
||||
to: data.title || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isExternalIdSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: document.externalId,
|
||||
to: data.externalId || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isGlobalAccessSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalAccessAuth,
|
||||
to: newGlobalAccessAuth,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isGlobalActionSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalActionAuth,
|
||||
to: newGlobalActionAuth,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDocumentVisibilitySame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: document.visibility,
|
||||
to: data.visibility || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Early return if nothing is required.
|
||||
if (auditLogs.length === 0) {
|
||||
return document;
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: newGlobalAccessAuth,
|
||||
globalActionAuth: newGlobalActionAuth,
|
||||
});
|
||||
|
||||
const updatedDocument = await tx.document.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId,
|
||||
visibility: data.visibility as DocumentVisibility,
|
||||
authOptions,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.createMany({
|
||||
data: auditLogs,
|
||||
});
|
||||
|
||||
return updatedDocument;
|
||||
});
|
||||
};
|
||||
@ -1,23 +1,40 @@
|
||||
'use server';
|
||||
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
|
||||
export type UpdateDocumentOptions = {
|
||||
documentId: number;
|
||||
data: Prisma.DocumentUpdateInput;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
data?: {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||
};
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const updateDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
data,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentOptions) => {
|
||||
return await prisma.document.update({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
@ -36,8 +53,215 @@ export const updateDocument = async ({
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
data: {
|
||||
...data,
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (teamId) {
|
||||
const currentUserRole = document.team?.members[0]?.role;
|
||||
const isDocumentOwner = document.userId === userId;
|
||||
const requestedVisibility = data?.visibility;
|
||||
|
||||
if (!isDocumentOwner) {
|
||||
match(currentUserRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => {
|
||||
const allowedVisibilities: DocumentVisibility[] = [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
];
|
||||
|
||||
if (
|
||||
!allowedVisibilities.includes(document.visibility) ||
|
||||
(requestedVisibility && !allowedVisibilities.includes(requestedVisibility))
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
});
|
||||
}
|
||||
})
|
||||
.with(TeamMemberRole.MEMBER, () => {
|
||||
if (
|
||||
document.visibility !== DocumentVisibility.EVERYONE ||
|
||||
(requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
});
|
||||
}
|
||||
})
|
||||
.otherwise(() => {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If no data just return the document since this function is normally chained after a meta update.
|
||||
if (!data || Object.values(data).length === 0) {
|
||||
return document;
|
||||
}
|
||||
|
||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
});
|
||||
|
||||
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
|
||||
const documentGlobalActionAuth = documentAuthOption?.globalActionAuth ?? null;
|
||||
|
||||
// If the new global auth values aren't passed in, fallback to the current document values.
|
||||
const newGlobalAccessAuth =
|
||||
data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth;
|
||||
const newGlobalActionAuth =
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const isTitleSame = data.title === undefined || data.title === document.title;
|
||||
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
|
||||
const isGlobalAccessSame =
|
||||
documentGlobalAccessAuth === undefined || documentGlobalAccessAuth === newGlobalAccessAuth;
|
||||
const isGlobalActionSame =
|
||||
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
||||
const isDocumentVisibilitySame =
|
||||
data.visibility === undefined || data.visibility === document.visibility;
|
||||
|
||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||
|
||||
if (!isTitleSame && document.status !== DocumentStatus.DRAFT) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'You cannot update the title if the document has been sent',
|
||||
});
|
||||
}
|
||||
|
||||
if (!isTitleSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: document.title,
|
||||
to: data.title || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isExternalIdSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: document.externalId,
|
||||
to: data.externalId || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isGlobalAccessSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalAccessAuth,
|
||||
to: newGlobalAccessAuth,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isGlobalActionSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalActionAuth,
|
||||
to: newGlobalActionAuth,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDocumentVisibilitySame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: document.visibility,
|
||||
to: data.visibility || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Early return if nothing is required.
|
||||
if (auditLogs.length === 0) {
|
||||
return document;
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: newGlobalAccessAuth,
|
||||
globalActionAuth: newGlobalActionAuth,
|
||||
});
|
||||
|
||||
const updatedDocument = await tx.document.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId,
|
||||
visibility: data.visibility as DocumentVisibility,
|
||||
authOptions,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.createMany({
|
||||
data: auditLogs,
|
||||
});
|
||||
|
||||
return updatedDocument;
|
||||
});
|
||||
};
|
||||
|
||||
@ -45,7 +45,9 @@ export const validateFieldAuth = async ({
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid authentication values');
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Invalid authentication values',
|
||||
});
|
||||
}
|
||||
|
||||
return derivedRecipientActionAuth;
|
||||
|
||||
@ -2,12 +2,15 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
|
||||
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 { ReadStatus } from '@documenso/prisma/client';
|
||||
import { ReadStatus, SendStatus } from '@documenso/prisma/client';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import type { TDocumentAccessAuthTypes } from '../../types/document-auth';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { getDocumentAndRecipientByToken } from './get-document-by-token';
|
||||
|
||||
export type ViewedDocumentOptions = {
|
||||
token: string;
|
||||
@ -39,6 +42,8 @@ export const viewedDocument = async ({
|
||||
id: recipient.id,
|
||||
},
|
||||
data: {
|
||||
// This handles cases where distribution is done manually
|
||||
sendStatus: SendStatus.SENT,
|
||||
readStatus: ReadStatus.OPENED,
|
||||
},
|
||||
});
|
||||
@ -63,11 +68,23 @@ export const viewedDocument = async ({
|
||||
});
|
||||
});
|
||||
|
||||
const document = await getDocumentAndRecipientByToken({ token, requireAccessAuth: false });
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_OPENED,
|
||||
data: document,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(document)),
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user