mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 16:51:38 +10:00
feat: migrate templates and documents to envelope model
This commit is contained in:
@ -1,17 +1,21 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { EnvelopeType, type Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
|
||||
export interface FindDocumentsOptions {
|
||||
export interface AdminFindDocumentsOptions {
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
}
|
||||
|
||||
export const findDocuments = async ({ query, page = 1, perPage = 10 }: FindDocumentsOptions) => {
|
||||
const termFilters: Prisma.DocumentWhereInput | undefined = !query
|
||||
export const adminFindDocuments = async ({
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
}: AdminFindDocumentsOptions) => {
|
||||
const termFilters: Prisma.EnvelopeWhereInput | undefined = !query
|
||||
? undefined
|
||||
: {
|
||||
title: {
|
||||
@ -21,8 +25,9 @@ export const findDocuments = async ({ query, page = 1, perPage = 10 }: FindDocum
|
||||
};
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.document.findMany({
|
||||
prisma.envelope.findMany({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
...termFilters,
|
||||
},
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
@ -39,10 +44,17 @@ export const findDocuments = async ({ query, page = 1, perPage = 10 }: FindDocum
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.document.count({
|
||||
prisma.envelope.count({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
...termFilters,
|
||||
},
|
||||
}),
|
||||
@ -17,15 +17,18 @@ import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
export type SuperDeleteDocumentOptions = {
|
||||
id: number;
|
||||
export type AdminSuperDeleteDocumentOptions = {
|
||||
envelopeId: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDocumentOptions) => {
|
||||
const document = await prisma.document.findUnique({
|
||||
export const adminSuperDeleteDocument = async ({
|
||||
envelopeId,
|
||||
requestMetadata,
|
||||
}: AdminSuperDeleteDocumentOptions) => {
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: {
|
||||
id,
|
||||
id: envelopeId,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
@ -40,7 +43,7 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
@ -50,38 +53,38 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const { status, user } = document;
|
||||
const { status, user } = envelope;
|
||||
|
||||
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).documentDeleted;
|
||||
|
||||
// if the document is pending, send cancellation emails to all recipients
|
||||
if (
|
||||
status === DocumentStatus.PENDING &&
|
||||
document.recipients.length > 0 &&
|
||||
envelope.recipients.length > 0 &&
|
||||
isDocumentDeletedEmailEnabled
|
||||
) {
|
||||
await Promise.all(
|
||||
document.recipients.map(async (recipient) => {
|
||||
envelope.recipients.map(async (recipient) => {
|
||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const template = createElement(DocumentCancelTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail: user.email,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
const lang = envelope.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
@ -113,7 +116,7 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId: id,
|
||||
envelopeId,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||
user,
|
||||
requestMetadata,
|
||||
@ -123,6 +126,6 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
}),
|
||||
});
|
||||
|
||||
return await tx.document.delete({ where: { id } });
|
||||
return await tx.envelope.delete({ where: { id: envelopeId } });
|
||||
});
|
||||
};
|
||||
@ -1,8 +1,13 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
export const getDocumentStats = async () => {
|
||||
const counts = await prisma.document.groupBy({
|
||||
const counts = await prisma.envelope.groupBy({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
by: ['status'],
|
||||
_count: {
|
||||
_all: true,
|
||||
|
||||
@ -1,14 +1,22 @@
|
||||
import type { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetEntireDocumentOptions = {
|
||||
id: number;
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
|
||||
export type unsafeGetEntireEnvelopeOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
type: EnvelopeType;
|
||||
};
|
||||
|
||||
export const getEntireDocument = async ({ id }: GetEntireDocumentOptions) => {
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
/**
|
||||
* An unauthenticated function that returns the whole envelope
|
||||
*/
|
||||
export const unsafeGetEntireEnvelope = async ({ id, type }: unsafeGetEntireEnvelopeOptions) => {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: unsafeBuildEnvelopeIdQuery(id, type),
|
||||
include: {
|
||||
documentMeta: true,
|
||||
user: {
|
||||
@ -30,5 +38,11 @@ export const getEntireDocument = async ({ id }: GetEntireDocumentOptions) => {
|
||||
},
|
||||
});
|
||||
|
||||
return document;
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
return envelope;
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DocumentStatus, SubscriptionStatus } from '@prisma/client';
|
||||
import { DocumentStatus, EnvelopeType, SubscriptionStatus } from '@prisma/client';
|
||||
|
||||
import { kyselyPrisma, sql } from '@documenso/prisma';
|
||||
|
||||
@ -31,22 +31,23 @@ export async function getSigningVolume({
|
||||
.selectFrom('Subscription as s')
|
||||
.innerJoin('Organisation as o', 's.organisationId', 'o.id')
|
||||
.leftJoin('Team as t', 'o.id', 't.organisationId')
|
||||
.leftJoin('Document as d', (join) =>
|
||||
.leftJoin('Envelope as e', (join) =>
|
||||
join
|
||||
.onRef('t.id', '=', 'd.teamId')
|
||||
.on('d.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
||||
.on('d.deletedAt', 'is', null),
|
||||
.onRef('t.id', '=', 'e.teamId')
|
||||
.on('e.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
||||
.on('e.deletedAt', 'is', null),
|
||||
)
|
||||
.where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`)
|
||||
.where((eb) =>
|
||||
eb.or([eb('o.name', 'ilike', `%${search}%`), eb('t.name', 'ilike', `%${search}%`)]),
|
||||
)
|
||||
.where('e.type', '=', EnvelopeType.DOCUMENT)
|
||||
.select([
|
||||
's.id as id',
|
||||
's.createdAt as createdAt',
|
||||
's.planId as planId',
|
||||
sql<string>`COALESCE(o.name, 'Unknown')`.as('name'),
|
||||
sql<number>`COUNT(DISTINCT d.id)`.as('signingVolume'),
|
||||
sql<number>`COUNT(DISTINCT e.id)`.as('signingVolume'),
|
||||
])
|
||||
.groupBy(['s.id', 'o.name']);
|
||||
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
|
||||
import {
|
||||
type DocumentDistributionMethod,
|
||||
type DocumentSigningOrder,
|
||||
EnvelopeType,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
@ -11,16 +15,16 @@ import { prisma } from '@documenso/prisma';
|
||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type CreateDocumentMetaOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
subject?: string;
|
||||
message?: string;
|
||||
timezone?: string;
|
||||
password?: string;
|
||||
dateFormat?: string;
|
||||
redirectUrl?: string;
|
||||
emailId?: string | null;
|
||||
@ -36,15 +40,14 @@ export type CreateDocumentMetaOptions = {
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const upsertDocumentMeta = async ({
|
||||
export const updateDocumentMeta = async ({
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
subject,
|
||||
message,
|
||||
timezone,
|
||||
dateFormat,
|
||||
documentId,
|
||||
password,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
@ -58,26 +61,27 @@ export const upsertDocumentMeta = async ({
|
||||
language,
|
||||
requestMetadata,
|
||||
}: CreateDocumentMetaOptions) => {
|
||||
const { documentWhereInput, team } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput, team } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: null, // Allow updating both documents and templates meta.
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const { documentMeta: originalDocumentMeta } = document;
|
||||
const { documentMeta: originalDocumentMeta } = envelope;
|
||||
|
||||
// Validate the emailId belongs to the organisation.
|
||||
if (emailId) {
|
||||
@ -96,33 +100,13 @@ export const upsertDocumentMeta = async ({
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const upsertedDocumentMeta = await tx.documentMeta.upsert({
|
||||
const upsertedDocumentMeta = await tx.documentMeta.update({
|
||||
where: {
|
||||
documentId,
|
||||
id: envelope.documentMetaId,
|
||||
},
|
||||
create: {
|
||||
data: {
|
||||
subject,
|
||||
message,
|
||||
password,
|
||||
dateFormat,
|
||||
timezone,
|
||||
documentId,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
emailId,
|
||||
emailReplyTo,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
language,
|
||||
},
|
||||
update: {
|
||||
subject,
|
||||
message,
|
||||
password,
|
||||
dateFormat,
|
||||
timezone,
|
||||
redirectUrl,
|
||||
@ -141,11 +125,12 @@ export const upsertDocumentMeta = async ({
|
||||
|
||||
const changes = diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta);
|
||||
|
||||
if (changes.length > 0) {
|
||||
// Create audit logs only for document type envelopes.
|
||||
if (changes.length > 0 && envelope.type === EnvelopeType.DOCUMENT) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
changes: diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta),
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
@ -22,9 +23,11 @@ import type { TRecipientAccessAuth, TRecipientActionAuth } from '../../types/doc
|
||||
import { DocumentAuth } from '../../types/document-auth';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapSecondaryIdToDocumentId, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { isRecipientAuthorized } from './is-recipient-authorized';
|
||||
@ -32,7 +35,7 @@ import { sendPendingEmail } from './send-pending-email';
|
||||
|
||||
export type CompleteDocumentWithTokenOptions = {
|
||||
token: string;
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
userId?: number;
|
||||
authOptions?: TRecipientActionAuth;
|
||||
accessAuthOptions?: TRecipientAccessAuth;
|
||||
@ -43,10 +46,17 @@ export type CompleteDocumentWithTokenOptions = {
|
||||
};
|
||||
};
|
||||
|
||||
const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => {
|
||||
return await prisma.document.findFirstOrThrow({
|
||||
export const completeDocumentWithToken = async ({
|
||||
token,
|
||||
id,
|
||||
userId,
|
||||
accessAuthOptions,
|
||||
requestMetadata,
|
||||
nextSigner,
|
||||
}: CompleteDocumentWithTokenOptions) => {
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
@ -62,27 +72,18 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const completeDocumentWithToken = async ({
|
||||
token,
|
||||
documentId,
|
||||
userId,
|
||||
accessAuthOptions,
|
||||
requestMetadata,
|
||||
nextSigner,
|
||||
}: CompleteDocumentWithTokenOptions) => {
|
||||
const document = await getDocument({ token, documentId });
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
if (document.status !== DocumentStatus.PENDING) {
|
||||
throw new Error(`Document ${document.id} must be pending`);
|
||||
if (envelope.status !== DocumentStatus.PENDING) {
|
||||
throw new Error(`Document ${envelope.id} must be pending`);
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error(`Document ${document.id} has no recipient with token ${token}`);
|
||||
if (envelope.recipients.length === 0) {
|
||||
throw new Error(`Document ${envelope.id} has no recipient with token ${token}`);
|
||||
}
|
||||
|
||||
const [recipient] = document.recipients;
|
||||
const [recipient] = envelope.recipients;
|
||||
|
||||
if (recipient.signingStatus === SigningStatus.SIGNED) {
|
||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||
@ -95,7 +96,7 @@ export const completeDocumentWithToken = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
if (envelope.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
||||
|
||||
if (!isRecipientsTurn) {
|
||||
@ -107,7 +108,7 @@ export const completeDocumentWithToken = async ({
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id, // Todo: Envelopes - Need to support multi docs.
|
||||
recipientId: recipient.id,
|
||||
},
|
||||
});
|
||||
@ -118,7 +119,7 @@ export const completeDocumentWithToken = async ({
|
||||
|
||||
// Check ACCESS AUTH 2FA validation during document completion
|
||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
documentAuth: envelope.authOptions,
|
||||
recipientAuth: recipient.authOptions,
|
||||
});
|
||||
|
||||
@ -131,7 +132,7 @@ export const completeDocumentWithToken = async ({
|
||||
|
||||
const isValid = await isRecipientAuthorized({
|
||||
type: 'ACCESS_2FA',
|
||||
documentAuthOptions: document.authOptions,
|
||||
documentAuthOptions: envelope.authOptions,
|
||||
recipient: recipient,
|
||||
userId, // Can be undefined for non-account recipients
|
||||
authOptions: accessAuthOptions,
|
||||
@ -141,7 +142,7 @@ export const completeDocumentWithToken = async ({
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_FAILED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
data: {
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
@ -158,7 +159,7 @@ export const completeDocumentWithToken = async ({
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_VALIDATED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
data: {
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
@ -180,14 +181,14 @@ export const completeDocumentWithToken = async ({
|
||||
});
|
||||
|
||||
const authOptions = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
documentAuth: envelope.authOptions,
|
||||
recipientAuth: recipient.authOptions,
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
@ -207,7 +208,7 @@ export const completeDocumentWithToken = async ({
|
||||
await jobs.triggerJob({
|
||||
name: 'send.recipient.signed.email',
|
||||
payload: {
|
||||
documentId: document.id,
|
||||
documentId: legacyDocumentId,
|
||||
recipientId: recipient.id,
|
||||
},
|
||||
});
|
||||
@ -221,7 +222,7 @@ export const completeDocumentWithToken = async ({
|
||||
role: true,
|
||||
},
|
||||
where: {
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
signingStatus: {
|
||||
not: SigningStatus.SIGNED,
|
||||
},
|
||||
@ -235,17 +236,17 @@ export const completeDocumentWithToken = async ({
|
||||
});
|
||||
|
||||
if (pendingRecipients.length > 0) {
|
||||
await sendPendingEmail({ documentId, recipientId: recipient.id });
|
||||
await sendPendingEmail({ id, recipientId: recipient.id });
|
||||
|
||||
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
if (envelope.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
const [nextRecipient] = pendingRecipients;
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
if (nextSigner && document.documentMeta?.allowDictateNextSigner) {
|
||||
if (nextSigner && envelope.documentMeta?.allowDictateNextSigner) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
@ -277,7 +278,7 @@ export const completeDocumentWithToken = async ({
|
||||
where: { id: nextRecipient.id },
|
||||
data: {
|
||||
sendStatus: SendStatus.SENT,
|
||||
...(nextSigner && document.documentMeta?.allowDictateNextSigner
|
||||
...(nextSigner && envelope.documentMeta?.allowDictateNextSigner
|
||||
? {
|
||||
name: nextSigner.name,
|
||||
email: nextSigner.email,
|
||||
@ -289,8 +290,8 @@ export const completeDocumentWithToken = async ({
|
||||
await jobs.triggerJob({
|
||||
name: 'send.signing.requested.email',
|
||||
payload: {
|
||||
userId: document.userId,
|
||||
documentId: document.id,
|
||||
userId: envelope.userId,
|
||||
documentId: legacyDocumentId,
|
||||
recipientId: nextRecipient.id,
|
||||
requestMetadata,
|
||||
},
|
||||
@ -299,9 +300,9 @@ export const completeDocumentWithToken = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const haveAllRecipientsSigned = await prisma.document.findFirst({
|
||||
const haveAllRecipientsSigned = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
recipients: {
|
||||
every: {
|
||||
OR: [{ signingStatus: SigningStatus.SIGNED }, { role: RecipientRole.CC }],
|
||||
@ -314,15 +315,16 @@ export const completeDocumentWithToken = async ({
|
||||
await jobs.triggerJob({
|
||||
name: 'internal.seal-document',
|
||||
payload: {
|
||||
documentId: document.id,
|
||||
documentId: legacyDocumentId,
|
||||
requestMetadata,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const updatedDocument = await prisma.document.findFirstOrThrow({
|
||||
const updatedDocument = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
@ -332,7 +334,7 @@ export const completeDocumentWithToken = async ({
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_SIGNED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: updatedDocument.userId,
|
||||
teamId: updatedDocument.teamId ?? undefined,
|
||||
});
|
||||
|
||||
@ -1,171 +0,0 @@
|
||||
import { DocumentSource, WebhookTriggerEvents } from '@prisma/client';
|
||||
import type { DocumentVisibility } from '@prisma/client';
|
||||
|
||||
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 { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { prefixedId } from '../../universal/id';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentOptions = {
|
||||
title: string;
|
||||
externalId?: string | null;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentDataId: string;
|
||||
formValues?: Record<string, string | number | boolean>;
|
||||
normalizePdf?: boolean;
|
||||
timezone?: string;
|
||||
userTimezone?: string;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
export const createDocument = async ({
|
||||
userId,
|
||||
title,
|
||||
externalId,
|
||||
documentDataId,
|
||||
teamId,
|
||||
normalizePdf,
|
||||
formValues,
|
||||
requestMetadata,
|
||||
timezone,
|
||||
userTimezone,
|
||||
folderId,
|
||||
}: CreateDocumentOptions) => {
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
let folderVisibility: DocumentVisibility | undefined;
|
||||
|
||||
if (folderId) {
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: folderId,
|
||||
team: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
visibility: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
|
||||
folderVisibility = folder.visibility;
|
||||
}
|
||||
|
||||
if (normalizePdf) {
|
||||
const documentData = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
id: documentDataId,
|
||||
},
|
||||
});
|
||||
|
||||
if (documentData) {
|
||||
const buffer = await getFileServerSide(documentData);
|
||||
|
||||
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer));
|
||||
|
||||
const newDocumentData = await putPdfFileServerSide({
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// userTimezone is last because it's always passed in regardless of the organisation/team settings
|
||||
// for uploads from the frontend
|
||||
const timezoneToUse = timezone || settings.documentTimezone || userTimezone;
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.create({
|
||||
data: {
|
||||
title,
|
||||
qrToken: prefixedId('qr'),
|
||||
externalId,
|
||||
documentDataId,
|
||||
userId,
|
||||
teamId,
|
||||
folderId,
|
||||
visibility:
|
||||
folderVisibility ??
|
||||
determineDocumentVisibility(settings.documentVisibility, team.currentTeamRole),
|
||||
formValues,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
create: extractDerivedDocumentMeta(settings, {
|
||||
timezone: timezoneToUse,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdDocument) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return createdDocument;
|
||||
});
|
||||
};
|
||||
@ -1,8 +1,8 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { Document, DocumentMeta, Recipient, User } from '@prisma/client';
|
||||
import { DocumentStatus, SendStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
import type { DocumentMeta, Envelope, Recipient, User } from '@prisma/client';
|
||||
import { DocumentStatus, EnvelopeType, SendStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||
@ -15,11 +15,12 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { getMemberRoles } from '../team/get-member-roles';
|
||||
@ -50,24 +51,23 @@ export const deleteDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const document = await prisma.document.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
// Note: This is an unsafe request, we validate the ownership later in the function.
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: unsafeBuildEnvelopeIdQuery({ type: 'documentId', id }, EnvelopeType.DOCUMENT),
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isUserTeamMember = await getMemberRoles({
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
@ -76,8 +76,8 @@ export const deleteDocument = async ({
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
const isUserOwner = document.userId === userId;
|
||||
const userRecipient = document.recipients.find((recipient) => recipient.email === user.email);
|
||||
const isUserOwner = envelope.userId === userId;
|
||||
const userRecipient = envelope.recipients.find((recipient) => recipient.email === user.email);
|
||||
|
||||
if (!isUserOwner && !isUserTeamMember && !userRecipient) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
@ -88,7 +88,7 @@ export const deleteDocument = async ({
|
||||
// Handle hard or soft deleting the actual document if user has permission.
|
||||
if (isUserOwner || isUserTeamMember) {
|
||||
await handleDocumentOwnerDelete({
|
||||
document,
|
||||
envelope,
|
||||
user,
|
||||
requestMetadata,
|
||||
});
|
||||
@ -113,27 +113,16 @@ export const deleteDocument = async ({
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CANCELLED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(document)),
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
// Return partial document for API v1 response.
|
||||
return {
|
||||
id: document.id,
|
||||
userId: document.userId,
|
||||
teamId: document.teamId,
|
||||
title: document.title,
|
||||
status: document.status,
|
||||
documentDataId: document.documentDataId,
|
||||
createdAt: document.createdAt,
|
||||
updatedAt: document.updatedAt,
|
||||
completedAt: document.completedAt,
|
||||
};
|
||||
return envelope;
|
||||
};
|
||||
|
||||
type HandleDocumentOwnerDeleteOptions = {
|
||||
document: Document & {
|
||||
envelope: Envelope & {
|
||||
recipients: Recipient[];
|
||||
documentMeta: DocumentMeta | null;
|
||||
};
|
||||
@ -142,11 +131,11 @@ type HandleDocumentOwnerDeleteOptions = {
|
||||
};
|
||||
|
||||
const handleDocumentOwnerDelete = async ({
|
||||
document,
|
||||
envelope,
|
||||
user,
|
||||
requestMetadata,
|
||||
}: HandleDocumentOwnerDeleteOptions) => {
|
||||
if (document.deletedAt) {
|
||||
if (envelope.deletedAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -154,17 +143,17 @@ const handleDocumentOwnerDelete = async ({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
// Soft delete completed documents.
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (isDocumentCompleted(envelope.status)) {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
@ -173,9 +162,9 @@ const handleDocumentOwnerDelete = async ({
|
||||
}),
|
||||
});
|
||||
|
||||
return await tx.document.update({
|
||||
return await tx.envelope.update({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date().toISOString(),
|
||||
@ -185,12 +174,12 @@ const handleDocumentOwnerDelete = async ({
|
||||
}
|
||||
|
||||
// Hard delete draft and pending documents.
|
||||
const deletedDocument = await prisma.$transaction(async (tx) => {
|
||||
const deletedEnvelope = await prisma.$transaction(async (tx) => {
|
||||
// Currently redundant since deleting a document will delete the audit logs.
|
||||
// However may be useful if we disassociate audit logs and documents if required.
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
@ -199,9 +188,9 @@ const handleDocumentOwnerDelete = async ({
|
||||
}),
|
||||
});
|
||||
|
||||
return await tx.document.delete({
|
||||
return await tx.envelope.delete({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
status: {
|
||||
not: DocumentStatus.COMPLETED,
|
||||
},
|
||||
@ -209,17 +198,17 @@ const handleDocumentOwnerDelete = async ({
|
||||
});
|
||||
});
|
||||
|
||||
const isDocumentDeleteEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
const isEnvelopeDeleteEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
envelope.documentMeta,
|
||||
).documentDeleted;
|
||||
|
||||
if (!isDocumentDeleteEmailEnabled) {
|
||||
return deletedDocument;
|
||||
if (!isEnvelopeDeleteEmailEnabled) {
|
||||
return deletedEnvelope;
|
||||
}
|
||||
|
||||
// Send cancellation emails to recipients.
|
||||
await Promise.all(
|
||||
document.recipients.map(async (recipient) => {
|
||||
envelope.recipients.map(async (recipient) => {
|
||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||
return;
|
||||
}
|
||||
@ -227,7 +216,7 @@ const handleDocumentOwnerDelete = async ({
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(DocumentCancelTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail: user.email,
|
||||
assetBaseUrl,
|
||||
@ -258,5 +247,5 @@ const handleDocumentOwnerDelete = async ({
|
||||
}),
|
||||
);
|
||||
|
||||
return deletedDocument;
|
||||
return deletedEnvelope;
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Prisma, Recipient } from '@prisma/client';
|
||||
import { DocumentSource, WebhookTriggerEvents } from '@prisma/client';
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { DocumentSource, EnvelopeType, WebhookTriggerEvents } from '@prisma/client';
|
||||
import { omit } from 'remeda';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@ -7,39 +7,42 @@ import { prisma } from '@documenso/prisma';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { nanoid, prefixedId } from '../../universal/id';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
import { incrementDocumentId } from '../envelope/increment-id';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export interface DuplicateDocumentOptions {
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
}
|
||||
|
||||
export const duplicateDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: DuplicateDocumentOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
export const duplicateDocument = async ({ id, userId, teamId }: DuplicateDocumentOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
select: {
|
||||
title: true,
|
||||
userId: true,
|
||||
documentData: {
|
||||
select: {
|
||||
data: true,
|
||||
initialData: true,
|
||||
type: true,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: {
|
||||
select: {
|
||||
data: true,
|
||||
initialData: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
authOptions: true,
|
||||
@ -54,44 +57,36 @@ export const duplicateDocument = async ({
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const documentData = await prisma.documentData.create({
|
||||
const { documentId, formattedDocumentId } = await incrementDocumentId();
|
||||
|
||||
const createdDocumentMeta = await prisma.documentMeta.create({
|
||||
data: {
|
||||
type: document.documentData.type,
|
||||
data: document.documentData.initialData,
|
||||
initialData: document.documentData.initialData,
|
||||
...omit(envelope.documentMeta, ['id']),
|
||||
emailSettings: envelope.documentMeta.emailSettings || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
let documentMeta: Prisma.DocumentCreateArgs['data']['documentMeta'] | undefined = undefined;
|
||||
|
||||
if (document.documentMeta) {
|
||||
documentMeta = {
|
||||
create: {
|
||||
...omit(document.documentMeta, ['id', 'documentId']),
|
||||
emailSettings: document.documentMeta.emailSettings || undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const createdDocument = await prisma.document.create({
|
||||
const duplicatedEnvelope = await prisma.envelope.create({
|
||||
data: {
|
||||
userId: document.userId,
|
||||
teamId: teamId,
|
||||
title: document.title,
|
||||
documentDataId: documentData.id,
|
||||
authOptions: document.authOptions || undefined,
|
||||
visibility: document.visibility,
|
||||
qrToken: prefixedId('qr'),
|
||||
documentMeta,
|
||||
id: prefixedId('envelope'),
|
||||
secondaryId: formattedDocumentId,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
title: envelope.title,
|
||||
documentMetaId: createdDocumentMeta.id,
|
||||
authOptions: envelope.authOptions || undefined,
|
||||
visibility: envelope.visibility,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
},
|
||||
include: {
|
||||
@ -100,53 +95,86 @@ export const duplicateDocument = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const recipientsToCreate = document.recipients.map((recipient) => ({
|
||||
documentId: createdDocument.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
fields: {
|
||||
createMany: {
|
||||
data: recipient.fields.map((field) => ({
|
||||
documentId: createdDocument.id,
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
|
||||
})),
|
||||
},
|
||||
},
|
||||
}));
|
||||
// Key = original envelope item ID
|
||||
// Value = duplicated envelope item ID.
|
||||
const oldEnvelopeItemToNewEnvelopeItemIdMap: Record<string, string> = {};
|
||||
|
||||
// Duplicate the envelope items.
|
||||
await Promise.all(
|
||||
envelope.envelopeItems.map(async (envelopeItem) => {
|
||||
const duplicatedDocumentData = await prisma.documentData.create({
|
||||
data: {
|
||||
type: envelopeItem.documentData.type,
|
||||
data: envelopeItem.documentData.initialData,
|
||||
initialData: envelopeItem.documentData.initialData,
|
||||
},
|
||||
});
|
||||
|
||||
const duplicatedEnvelopeItem = await prisma.envelopeItem.create({
|
||||
data: {
|
||||
id: prefixedId('envelope_item'),
|
||||
title: envelopeItem.title,
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
documentDataId: duplicatedDocumentData.id,
|
||||
},
|
||||
});
|
||||
|
||||
oldEnvelopeItemToNewEnvelopeItemIdMap[envelopeItem.id] = duplicatedEnvelopeItem.id;
|
||||
}),
|
||||
);
|
||||
|
||||
const recipients: Recipient[] = [];
|
||||
|
||||
for (const recipientData of recipientsToCreate) {
|
||||
const newRecipient = await prisma.recipient.create({
|
||||
data: recipientData,
|
||||
for (const recipient of envelope.recipients) {
|
||||
const duplicatedRecipient = await prisma.recipient.create({
|
||||
data: {
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
fields: {
|
||||
createMany: {
|
||||
data: recipient.fields.map((field) => ({
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId],
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
recipients.push(newRecipient);
|
||||
recipients.push(duplicatedRecipient);
|
||||
}
|
||||
|
||||
const refetchedEnvelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: duplicatedEnvelope.id,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse({
|
||||
...mapDocumentToWebhookDocumentPayload(createdDocument),
|
||||
recipients,
|
||||
documentMeta: createdDocument.documentMeta,
|
||||
}),
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(refetchedEnvelope)),
|
||||
userId: userId,
|
||||
teamId: teamId,
|
||||
});
|
||||
|
||||
return {
|
||||
documentId: createdDocument.id,
|
||||
documentId,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { DocumentAuditLog, Prisma } from '@prisma/client';
|
||||
import { type DocumentAuditLog, EnvelopeType, type Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -6,7 +6,7 @@ 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';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface FindDocumentAuditLogsOptions {
|
||||
userId: number;
|
||||
@ -35,22 +35,26 @@ export const findDocumentAuditLogs = async ({
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const whereClause: Prisma.DocumentAuditLogWhereInput = {
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
};
|
||||
|
||||
// Filter events down to what we consider recent activity.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Document, DocumentSource, Prisma, Team, TeamEmail, User } from '@prisma/client';
|
||||
import { RecipientRole, SigningStatus, TeamMemberRole } from '@prisma/client';
|
||||
import type { DocumentSource, Envelope, Prisma, Team, TeamEmail, User } from '@prisma/client';
|
||||
import { EnvelopeType, RecipientRole, SigningStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -22,7 +22,7 @@ export type FindDocumentsOptions = {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
column: keyof Omit<Document, 'document'>;
|
||||
column: keyof Pick<Envelope, 'createdAt'>;
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
period?: PeriodSelectorValue;
|
||||
@ -69,7 +69,7 @@ export const findDocuments = async ({
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
const teamMemberRole = team?.currentTeamRole ?? null;
|
||||
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
const searchFilter: Prisma.EnvelopeWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{ externalId: { contains: query, mode: 'insensitive' } },
|
||||
@ -111,7 +111,7 @@ export const findDocuments = async ({
|
||||
},
|
||||
];
|
||||
|
||||
let filters: Prisma.DocumentWhereInput | null = findDocumentsFilter(status, user, folderId);
|
||||
let filters: Prisma.EnvelopeWhereInput | null = findDocumentsFilter(status, user, folderId);
|
||||
|
||||
if (team) {
|
||||
filters = findTeamDocumentsFilter(status, team, visibilityFilters, folderId);
|
||||
@ -127,7 +127,7 @@ export const findDocuments = async ({
|
||||
};
|
||||
}
|
||||
|
||||
let deletedFilter: Prisma.DocumentWhereInput = {
|
||||
let deletedFilter: Prisma.EnvelopeWhereInput = {
|
||||
AND: {
|
||||
OR: [
|
||||
{
|
||||
@ -180,7 +180,7 @@ export const findDocuments = async ({
|
||||
};
|
||||
}
|
||||
|
||||
const whereAndClause: Prisma.DocumentWhereInput['AND'] = [
|
||||
const whereAndClause: Prisma.EnvelopeWhereInput['AND'] = [
|
||||
{ ...filters },
|
||||
{ ...deletedFilter },
|
||||
{ ...searchFilter },
|
||||
@ -198,7 +198,8 @@ export const findDocuments = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const whereClause: Prisma.DocumentWhereInput = {
|
||||
const whereClause: Prisma.EnvelopeWhereInput = {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
AND: whereAndClause,
|
||||
};
|
||||
|
||||
@ -225,7 +226,7 @@ export const findDocuments = async ({
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.document.findMany({
|
||||
prisma.envelope.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
@ -249,7 +250,7 @@ export const findDocuments = async ({
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.document.count({
|
||||
prisma.envelope.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
@ -275,7 +276,7 @@ const findDocumentsFilter = (
|
||||
user: Pick<User, 'id' | 'email' | 'name'>,
|
||||
folderId?: string | null,
|
||||
) => {
|
||||
return match<ExtendedDocumentStatus, Prisma.DocumentWhereInput>(status)
|
||||
return match<ExtendedDocumentStatus, Prisma.EnvelopeWhereInput>(status)
|
||||
.with(ExtendedDocumentStatus.ALL, () => ({
|
||||
OR: [
|
||||
{
|
||||
@ -414,14 +415,14 @@ const findDocumentsFilter = (
|
||||
const findTeamDocumentsFilter = (
|
||||
status: ExtendedDocumentStatus,
|
||||
team: Team & { teamEmail: TeamEmail | null },
|
||||
visibilityFilters: Prisma.DocumentWhereInput[],
|
||||
visibilityFilters: Prisma.EnvelopeWhereInput[],
|
||||
folderId?: string,
|
||||
) => {
|
||||
const teamEmail = team.teamEmail?.email ?? null;
|
||||
|
||||
return match<ExtendedDocumentStatus, Prisma.DocumentWhereInput | null>(status)
|
||||
return match<ExtendedDocumentStatus, Prisma.EnvelopeWhereInput | null>(status)
|
||||
.with(ExtendedDocumentStatus.ALL, () => {
|
||||
const filter: Prisma.DocumentWhereInput = {
|
||||
const filter: Prisma.EnvelopeWhereInput = {
|
||||
// Filter to display all documents that belong to the team.
|
||||
OR: [
|
||||
{
|
||||
@ -483,7 +484,7 @@ const findTeamDocumentsFilter = (
|
||||
};
|
||||
})
|
||||
.with(ExtendedDocumentStatus.DRAFT, () => {
|
||||
const filter: Prisma.DocumentWhereInput = {
|
||||
const filter: Prisma.EnvelopeWhereInput = {
|
||||
OR: [
|
||||
{
|
||||
teamId: team.id,
|
||||
@ -508,7 +509,7 @@ const findTeamDocumentsFilter = (
|
||||
return filter;
|
||||
})
|
||||
.with(ExtendedDocumentStatus.PENDING, () => {
|
||||
const filter: Prisma.DocumentWhereInput = {
|
||||
const filter: Prisma.EnvelopeWhereInput = {
|
||||
OR: [
|
||||
{
|
||||
teamId: team.id,
|
||||
@ -550,7 +551,7 @@ const findTeamDocumentsFilter = (
|
||||
return filter;
|
||||
})
|
||||
.with(ExtendedDocumentStatus.COMPLETED, () => {
|
||||
const filter: Prisma.DocumentWhereInput = {
|
||||
const filter: Prisma.EnvelopeWhereInput = {
|
||||
status: ExtendedDocumentStatus.COMPLETED,
|
||||
OR: [
|
||||
{
|
||||
@ -582,7 +583,7 @@ const findTeamDocumentsFilter = (
|
||||
return filter;
|
||||
})
|
||||
.with(ExtendedDocumentStatus.REJECTED, () => {
|
||||
const filter: Prisma.DocumentWhereInput = {
|
||||
const filter: Prisma.EnvelopeWhereInput = {
|
||||
status: ExtendedDocumentStatus.REJECTED,
|
||||
OR: [
|
||||
{
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||
|
||||
export type GetDocumentByAccessTokenOptions = {
|
||||
token: string;
|
||||
};
|
||||
@ -9,30 +13,54 @@ export const getDocumentByAccessToken = async ({ token }: GetDocumentByAccessTok
|
||||
throw new Error('Missing token');
|
||||
}
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
const result = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
status: DocumentStatus.COMPLETED,
|
||||
qrToken: token,
|
||||
},
|
||||
// Do not provide extra information that is not needed.
|
||||
select: {
|
||||
id: true,
|
||||
secondaryId: true,
|
||||
title: true,
|
||||
completedAt: true,
|
||||
documentData: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
documentMeta: {
|
||||
envelopeItems: {
|
||||
select: {
|
||||
password: true,
|
||||
documentData: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
recipients: true,
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
// Todo: Envelopes
|
||||
if (!result.envelopeItems[0].documentData) {
|
||||
throw new Error('Missing document data');
|
||||
}
|
||||
|
||||
return {
|
||||
id: mapSecondaryIdToDocumentId(result.secondaryId),
|
||||
title: result.title,
|
||||
completedAt: result.completedAt,
|
||||
documentData: result.envelopeItems[0].documentData,
|
||||
recipientCount: result._count.recipients,
|
||||
documentTeamUrl: result.team.url,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,156 +0,0 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type GetDocumentByIdOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
export const getDocumentById = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
folderId,
|
||||
}: GetDocumentByIdOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
...documentWhereInput,
|
||||
folderId,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document could not be found',
|
||||
});
|
||||
}
|
||||
|
||||
return document;
|
||||
};
|
||||
|
||||
export type GetDocumentWhereInputOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the where input for a given Prisma document query.
|
||||
*
|
||||
* This will return a query that allows a user to get a document if they have valid access to it.
|
||||
*/
|
||||
export const getDocumentWhereInput = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetDocumentWhereInputOptions) => {
|
||||
const team = await getTeamById({ teamId, userId });
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const teamVisibilityFilters = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
DocumentVisibility.ADMIN,
|
||||
])
|
||||
.with(TeamMemberRole.MANAGER, () => [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
])
|
||||
.otherwise(() => [DocumentVisibility.EVERYONE]);
|
||||
|
||||
const documentOrInput: Prisma.DocumentWhereInput[] = [
|
||||
// Allow access if they own the document.
|
||||
{
|
||||
userId,
|
||||
},
|
||||
// Or, if they belong to the team that the document is associated with.
|
||||
{
|
||||
visibility: {
|
||||
in: teamVisibilityFilters,
|
||||
},
|
||||
teamId: team.id,
|
||||
},
|
||||
// Or, if they are a recipient of the document.
|
||||
{
|
||||
status: {
|
||||
not: DocumentStatus.DRAFT,
|
||||
},
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Allow access to documents sent to or from the team email.
|
||||
if (team.teamEmail) {
|
||||
documentOrInput.push(
|
||||
{
|
||||
recipients: {
|
||||
some: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const documentWhereInput: Prisma.DocumentWhereUniqueInput = {
|
||||
id: documentId,
|
||||
OR: documentOrInput,
|
||||
};
|
||||
|
||||
return {
|
||||
documentWhereInput,
|
||||
team,
|
||||
};
|
||||
};
|
||||
@ -1,8 +1,10 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAuthMethods } from '../../types/document-auth';
|
||||
import { mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||
import { isRecipientAuthorized } from './is-recipient-authorized';
|
||||
|
||||
export interface GetDocumentAndSenderByTokenOptions {
|
||||
@ -39,8 +41,9 @@ export const getDocumentByToken = async ({ token }: GetDocumentByTokenOptions) =
|
||||
throw new Error('Missing token');
|
||||
}
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
const result = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
@ -64,8 +67,9 @@ export const getDocumentAndSenderByToken = async ({
|
||||
throw new Error('Missing token');
|
||||
}
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
const result = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
@ -80,13 +84,17 @@ export const getDocumentAndSenderByToken = async ({
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: {
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
@ -102,6 +110,13 @@ export const getDocumentAndSenderByToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
// Todo: Envelopes
|
||||
const firstDocumentData = result.envelopeItems[0].documentData;
|
||||
|
||||
if (!firstDocumentData) {
|
||||
throw new Error('Missing document data');
|
||||
}
|
||||
|
||||
const recipient = result.recipients[0];
|
||||
|
||||
// Sanity check, should not be possible.
|
||||
@ -127,6 +142,8 @@ export const getDocumentAndSenderByToken = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(result.secondaryId);
|
||||
|
||||
return {
|
||||
...result,
|
||||
user: {
|
||||
@ -134,64 +151,7 @@ export const getDocumentAndSenderByToken = async ({
|
||||
email: result.user.email,
|
||||
name: result.user.name,
|
||||
},
|
||||
documentData: firstDocumentData,
|
||||
id: legacyDocumentId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a Document and a Recipient by the recipient token.
|
||||
*/
|
||||
export const getDocumentAndRecipientByToken = async ({
|
||||
token,
|
||||
userId,
|
||||
accessAuth,
|
||||
requireAccessAuth = true,
|
||||
}: GetDocumentAndRecipientByTokenOptions): Promise<DocumentWithRecipient> => {
|
||||
if (!token) {
|
||||
throw new Error('Missing token');
|
||||
}
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
documentData: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [recipient] = result.recipients;
|
||||
|
||||
// Sanity check, should not be possible.
|
||||
if (!recipient) {
|
||||
throw new Error('Missing recipient');
|
||||
}
|
||||
|
||||
let documentAccessValid = true;
|
||||
|
||||
if (requireAccessAuth) {
|
||||
documentAccessValid = await isRecipientAuthorized({
|
||||
type: 'ACCESS',
|
||||
documentAuthOptions: result.authOptions,
|
||||
recipient,
|
||||
userId,
|
||||
authOptions: accessAuth,
|
||||
});
|
||||
}
|
||||
|
||||
if (!documentAccessValid) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Invalid access values',
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@ -4,15 +4,15 @@ import { DOCUMENT_AUDIT_LOG_TYPE, DOCUMENT_EMAIL_TYPE } from '../../types/docume
|
||||
import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export type GetDocumentCertificateAuditLogsOptions = {
|
||||
id: number;
|
||||
envelopeId: string;
|
||||
};
|
||||
|
||||
export const getDocumentCertificateAuditLogs = async ({
|
||||
id,
|
||||
envelopeId,
|
||||
}: GetDocumentCertificateAuditLogsOptions) => {
|
||||
const rawAuditLogs = await prisma.documentAuditLog.findMany({
|
||||
where: {
|
||||
documentId: id,
|
||||
envelopeId,
|
||||
type: {
|
||||
in: [
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
|
||||
@ -1,67 +1,52 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||
import { getEnvelopeById } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type GetDocumentWithDetailsByIdOptions = {
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const getDocumentWithDetailsById = async ({
|
||||
documentId,
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetDocumentWithDetailsByIdOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const envelope = await getEnvelopeById({
|
||||
id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
...documentWhereInput,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
folder: true,
|
||||
fields: {
|
||||
include: {
|
||||
signature: true,
|
||||
recipient: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
signingStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
// Todo: Envelopes
|
||||
const firstDocumentData = envelope.envelopeItems[0].documentData;
|
||||
|
||||
if (!firstDocumentData) {
|
||||
throw new Error('Document data not found');
|
||||
}
|
||||
|
||||
return document;
|
||||
return {
|
||||
...envelope,
|
||||
documentData: firstDocumentData,
|
||||
id: legacyDocumentId,
|
||||
fields: envelope.fields.map((field) => ({
|
||||
...field,
|
||||
documentId: legacyDocumentId,
|
||||
})),
|
||||
user: {
|
||||
id: envelope.userId,
|
||||
name: envelope.user.name,
|
||||
email: envelope.user.email,
|
||||
},
|
||||
team: {
|
||||
id: envelope.teamId,
|
||||
url: envelope.team.url,
|
||||
},
|
||||
recipients: envelope.recipients,
|
||||
};
|
||||
};
|
||||
|
||||
@ -7,7 +7,7 @@ export type GetRecipientOrSenderByShareLinkSlugOptions = {
|
||||
export const getRecipientOrSenderByShareLinkSlug = async ({
|
||||
slug,
|
||||
}: GetRecipientOrSenderByShareLinkSlugOptions) => {
|
||||
const { documentId, email } = await prisma.documentShareLink.findFirstOrThrow({
|
||||
const { envelopeId, email } = await prisma.documentShareLink.findFirstOrThrow({
|
||||
where: {
|
||||
slug,
|
||||
},
|
||||
@ -15,7 +15,7 @@ export const getRecipientOrSenderByShareLinkSlug = async ({
|
||||
|
||||
const sender = await prisma.user.findFirst({
|
||||
where: {
|
||||
documents: { some: { id: documentId } },
|
||||
envelopes: { some: { id: envelopeId } },
|
||||
email,
|
||||
},
|
||||
select: {
|
||||
@ -31,7 +31,7 @@ export const getRecipientOrSenderByShareLinkSlug = async ({
|
||||
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
documentId,
|
||||
envelopeId,
|
||||
email,
|
||||
},
|
||||
select: {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { EnvelopeType, TeamMemberRole } from '@prisma/client';
|
||||
import type { Prisma, User } from '@prisma/client';
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
@ -25,7 +25,7 @@ export const getStats = async ({
|
||||
folderId,
|
||||
...options
|
||||
}: GetStatsInput) => {
|
||||
let createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||
let createdAt: Prisma.EnvelopeWhereInput['createdAt'];
|
||||
|
||||
if (period) {
|
||||
const daysAgo = parseInt(period.replace(/d$/, ''), 10);
|
||||
@ -90,13 +90,13 @@ export const getStats = async ({
|
||||
|
||||
type GetCountsOption = {
|
||||
user: Pick<User, 'id' | 'email'>;
|
||||
createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||
createdAt: Prisma.EnvelopeWhereInput['createdAt'];
|
||||
search?: string;
|
||||
folderId?: string | null;
|
||||
};
|
||||
|
||||
const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption) => {
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
const searchFilter: Prisma.EnvelopeWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ recipients: { some: { name: { contains: search, mode: 'insensitive' } } } },
|
||||
@ -108,12 +108,13 @@ const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption)
|
||||
|
||||
return Promise.all([
|
||||
// Owner counts.
|
||||
prisma.document.groupBy({
|
||||
prisma.envelope.groupBy({
|
||||
by: ['status'],
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId: user.id,
|
||||
createdAt,
|
||||
deletedAt: null,
|
||||
@ -121,12 +122,13 @@ const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption)
|
||||
},
|
||||
}),
|
||||
// Not signed counts.
|
||||
prisma.document.groupBy({
|
||||
prisma.envelope.groupBy({
|
||||
by: ['status'],
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
recipients: {
|
||||
some: {
|
||||
@ -140,12 +142,13 @@ const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption)
|
||||
},
|
||||
}),
|
||||
// Has signed counts.
|
||||
prisma.document.groupBy({
|
||||
prisma.envelope.groupBy({
|
||||
by: ['status'],
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
createdAt,
|
||||
user: {
|
||||
email: {
|
||||
@ -186,7 +189,7 @@ type GetTeamCountsOption = {
|
||||
senderIds?: number[];
|
||||
currentUserEmail: string;
|
||||
userId: number;
|
||||
createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||
createdAt: Prisma.EnvelopeWhereInput['createdAt'];
|
||||
currentTeamMemberRole?: TeamMemberRole;
|
||||
search?: string;
|
||||
folderId?: string | null;
|
||||
@ -197,14 +200,14 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
|
||||
const senderIds = options.senderIds ?? [];
|
||||
|
||||
const userIdWhereClause: Prisma.DocumentWhereInput['userId'] =
|
||||
const userIdWhereClause: Prisma.EnvelopeWhereInput['userId'] =
|
||||
senderIds.length > 0
|
||||
? {
|
||||
in: senderIds,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
const searchFilter: Prisma.EnvelopeWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: options.search, mode: 'insensitive' } },
|
||||
{ recipients: { some: { name: { contains: options.search, mode: 'insensitive' } } } },
|
||||
@ -212,7 +215,8 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
],
|
||||
};
|
||||
|
||||
let ownerCountsWhereInput: Prisma.DocumentWhereInput = {
|
||||
let ownerCountsWhereInput: Prisma.EnvelopeWhereInput = {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId: userIdWhereClause,
|
||||
createdAt,
|
||||
teamId,
|
||||
@ -223,7 +227,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
let notSignedCountsGroupByArgs = null;
|
||||
let hasSignedCountsGroupByArgs = null;
|
||||
|
||||
const visibilityFiltersWhereInput: Prisma.DocumentWhereInput = {
|
||||
const visibilityFiltersWhereInput: Prisma.EnvelopeWhereInput = {
|
||||
AND: [
|
||||
{ deletedAt: null },
|
||||
{
|
||||
@ -267,6 +271,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
|
||||
if (teamEmail) {
|
||||
ownerCountsWhereInput = {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId: userIdWhereClause,
|
||||
createdAt,
|
||||
OR: [
|
||||
@ -288,6 +293,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
_all: true,
|
||||
},
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId: userIdWhereClause,
|
||||
createdAt,
|
||||
folderId,
|
||||
@ -301,7 +307,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
} satisfies Prisma.DocumentGroupByArgs;
|
||||
} satisfies Prisma.EnvelopeGroupByArgs;
|
||||
|
||||
hasSignedCountsGroupByArgs = {
|
||||
by: ['status'],
|
||||
@ -309,6 +315,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
_all: true,
|
||||
},
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId: userIdWhereClause,
|
||||
createdAt,
|
||||
folderId,
|
||||
@ -336,18 +343,18 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
} satisfies Prisma.DocumentGroupByArgs;
|
||||
} satisfies Prisma.EnvelopeGroupByArgs;
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
prisma.document.groupBy({
|
||||
prisma.envelope.groupBy({
|
||||
by: ['status'],
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
where: ownerCountsWhereInput,
|
||||
}),
|
||||
notSignedCountsGroupByArgs ? prisma.document.groupBy(notSignedCountsGroupByArgs) : [],
|
||||
hasSignedCountsGroupByArgs ? prisma.document.groupBy(hasSignedCountsGroupByArgs) : [],
|
||||
notSignedCountsGroupByArgs ? prisma.envelope.groupBy(notSignedCountsGroupByArgs) : [],
|
||||
hasSignedCountsGroupByArgs ? prisma.envelope.groupBy(hasSignedCountsGroupByArgs) : [],
|
||||
]);
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Document, Recipient } from '@prisma/client';
|
||||
import type { Envelope, Recipient } from '@prisma/client';
|
||||
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -17,8 +17,8 @@ import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
type IsRecipientAuthorizedOptions = {
|
||||
// !: Probably find a better name than 'ACCESS_2FA' if requirements change.
|
||||
type: 'ACCESS' | 'ACCESS_2FA' | 'ACTION';
|
||||
documentAuthOptions: Document['authOptions'];
|
||||
recipient: Pick<Recipient, 'authOptions' | 'email' | 'documentId'>;
|
||||
documentAuthOptions: Envelope['authOptions'];
|
||||
recipient: Pick<Recipient, 'authOptions' | 'email'>;
|
||||
|
||||
/**
|
||||
* The ID of the user who initiated the request.
|
||||
@ -125,6 +125,7 @@ export const isRecipientAuthorized = async ({
|
||||
}
|
||||
|
||||
if (type === 'ACCESS_2FA' && method === 'email') {
|
||||
// Todo: Envelopes - Need to pass in the secondary ID to parse the document ID for.
|
||||
if (!recipient.documentId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document ID is required for email 2FA verification',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { EnvelopeType, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { jobs } from '@documenso/lib/jobs/client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@ -7,17 +7,19 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapSecondaryIdToDocumentId, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
|
||||
export type RejectDocumentWithTokenOptions = {
|
||||
token: string;
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
reason: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export async function rejectDocumentWithToken({
|
||||
token,
|
||||
documentId,
|
||||
id,
|
||||
reason,
|
||||
requestMetadata,
|
||||
}: RejectDocumentWithTokenOptions) {
|
||||
@ -25,16 +27,16 @@ export async function rejectDocumentWithToken({
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
token,
|
||||
documentId,
|
||||
envelope: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
||||
},
|
||||
include: {
|
||||
document: true,
|
||||
envelope: true,
|
||||
},
|
||||
});
|
||||
|
||||
const document = recipient?.document;
|
||||
const envelope = recipient?.envelope;
|
||||
|
||||
if (!recipient || !document) {
|
||||
if (!recipient || !envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document or recipient not found',
|
||||
});
|
||||
@ -54,7 +56,7 @@ export async function rejectDocumentWithToken({
|
||||
}),
|
||||
prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
@ -72,11 +74,13 @@ export async function rejectDocumentWithToken({
|
||||
}),
|
||||
]);
|
||||
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
// Trigger the seal document job to process the document asynchronously
|
||||
await jobs.triggerJob({
|
||||
name: 'internal.seal-document',
|
||||
payload: {
|
||||
documentId,
|
||||
documentId: legacyDocumentId,
|
||||
requestMetadata,
|
||||
},
|
||||
});
|
||||
@ -86,7 +90,7 @@ export async function rejectDocumentWithToken({
|
||||
name: 'send.signing.rejected.emails',
|
||||
payload: {
|
||||
recipientId: recipient.id,
|
||||
documentId,
|
||||
documentId: legacyDocumentId,
|
||||
},
|
||||
});
|
||||
|
||||
@ -94,7 +98,7 @@ export async function rejectDocumentWithToken({
|
||||
await jobs.triggerJob({
|
||||
name: 'send.document.cancelled.emails',
|
||||
payload: {
|
||||
documentId,
|
||||
documentId: legacyDocumentId,
|
||||
cancellationReason: reason,
|
||||
requestMetadata,
|
||||
},
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DocumentStatus, OrganisationType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import {
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
OrganisationType,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||
@ -21,7 +27,7 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email'
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type ResendDocumentOptions = {
|
||||
documentId: number;
|
||||
@ -42,16 +48,25 @@ export const resendDocument = async ({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findUnique({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
@ -64,31 +79,29 @@ export const resendDocument = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const customEmail = document?.documentMeta;
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
if (envelope.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
if (document.status === DocumentStatus.DRAFT) {
|
||||
if (envelope.status === DocumentStatus.DRAFT) {
|
||||
throw new Error('Can not send draft document');
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (isDocumentCompleted(envelope.status)) {
|
||||
throw new Error('Can not send completed document');
|
||||
}
|
||||
|
||||
const recipientsToRemind = document.recipients.filter(
|
||||
const recipientsToRemind = envelope.recipients.filter(
|
||||
(recipient) =>
|
||||
recipients.includes(recipient.id) && recipient.signingStatus === SigningStatus.NOT_SIGNED,
|
||||
);
|
||||
|
||||
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).recipientSigningRequest;
|
||||
|
||||
if (!isRecipientSigningRequestEmailEnabled) {
|
||||
@ -100,9 +113,9 @@ export const resendDocument = async ({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
@ -122,42 +135,42 @@ export const resendDocument = async ({
|
||||
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
|
||||
.toLowerCase();
|
||||
|
||||
let emailMessage = customEmail?.message || '';
|
||||
let emailMessage = envelope.documentMeta.message || '';
|
||||
let emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} this document`);
|
||||
|
||||
if (selfSigner) {
|
||||
emailMessage = i18n._(
|
||||
msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`,
|
||||
msg`You have initiated the document ${`"${envelope.title}"`} that requires you to ${recipientActionVerb} it.`,
|
||||
);
|
||||
emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} your document`);
|
||||
}
|
||||
|
||||
if (organisationType === OrganisationType.ORGANISATION) {
|
||||
emailSubject = i18n._(
|
||||
msg`Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`,
|
||||
msg`Reminder: ${envelope.team.name} invited you to ${recipientActionVerb} a document`,
|
||||
);
|
||||
emailMessage =
|
||||
customEmail?.message ||
|
||||
envelope.documentMeta.message ||
|
||||
i18n._(
|
||||
msg`${user.name || user.email} on behalf of "${document.team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`,
|
||||
msg`${user.name || user.email} on behalf of "${envelope.team.name}" has invited you to ${recipientActionVerb} the document "${envelope.title}".`,
|
||||
);
|
||||
}
|
||||
|
||||
const customEmailTemplate = {
|
||||
'signer.name': name,
|
||||
'signer.email': email,
|
||||
'document.name': document.title,
|
||||
'document.name': envelope.title,
|
||||
};
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
|
||||
|
||||
const template = createElement(DocumentInviteEmailTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail:
|
||||
organisationType === OrganisationType.ORGANISATION
|
||||
? document.team?.teamEmail?.email || user.email
|
||||
? envelope.team?.teamEmail?.email || user.email
|
||||
: user.email,
|
||||
assetBaseUrl,
|
||||
signDocumentLink,
|
||||
@ -165,7 +178,7 @@ export const resendDocument = async ({
|
||||
role: recipient.role,
|
||||
selfSigner,
|
||||
organisationType,
|
||||
teamName: document.team?.name,
|
||||
teamName: envelope.team?.name,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
@ -189,9 +202,9 @@ export const resendDocument = async ({
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: customEmail?.subject
|
||||
subject: envelope.documentMeta.subject
|
||||
? renderCustomEmailTemplate(
|
||||
i18n._(msg`Reminder: ${customEmail.subject}`),
|
||||
i18n._(msg`Reminder: ${envelope.documentMeta.subject}`),
|
||||
customEmailTemplate,
|
||||
)
|
||||
: emailSubject,
|
||||
@ -202,7 +215,7 @@ export const resendDocument = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
emailType: recipientEmailType,
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
import { DocumentStatus, RecipientRole, SigningStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
import {
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@prisma/client';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'node:path';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
@ -11,12 +17,17 @@ import { signPdf } from '@documenso/signing';
|
||||
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
||||
import {
|
||||
type EnvelopeIdOptions,
|
||||
mapSecondaryIdToDocumentId,
|
||||
unsafeBuildEnvelopeIdQuery,
|
||||
} from '../../utils/envelope';
|
||||
import { getAuditLogsPdf } from '../htmltopdf/get-audit-logs-pdf';
|
||||
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
||||
import { addRejectionStampToPdf } from '../pdf/add-rejection-stamp-to-pdf';
|
||||
@ -30,49 +41,63 @@ import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { sendCompletedEmail } from './send-completed-email';
|
||||
|
||||
export type SealDocumentOptions = {
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
sendEmail?: boolean;
|
||||
isResealing?: boolean;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const sealDocument = async ({
|
||||
documentId,
|
||||
id,
|
||||
sendEmail = true,
|
||||
isResealing = false,
|
||||
requestMetadata,
|
||||
}: SealDocumentOptions) => {
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
||||
include: {
|
||||
documentData: true,
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
documentData: true,
|
||||
},
|
||||
include: {
|
||||
field: {
|
||||
include: {
|
||||
signature: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { documentData } = document;
|
||||
|
||||
if (!documentData) {
|
||||
throw new Error(`Document ${document.id} has no document data`);
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId: document.userId,
|
||||
teamId: document.teamId,
|
||||
});
|
||||
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
role: {
|
||||
not: RecipientRole.CC,
|
||||
recipients: {
|
||||
where: {
|
||||
role: {
|
||||
not: RecipientRole.CC,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Todo: Envelopes
|
||||
const envelopeItemToSeal = envelope.envelopeItems[0];
|
||||
|
||||
// Todo: Envelopes
|
||||
if (envelope.envelopeItems.length !== 1 || !envelopeItemToSeal) {
|
||||
throw new Error(`Document ${envelope.id} needs exactly 1 envelope item`);
|
||||
}
|
||||
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
const documentData = envelopeItemToSeal.documentData;
|
||||
const fields = envelopeItemToSeal.field; // Todo: Envelopes - This only takes in the first envelope item fields.
|
||||
const recipients = envelope.recipients;
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
});
|
||||
|
||||
// Determine if the document has been rejected by checking if any recipient has rejected it
|
||||
const rejectedRecipient = recipients.find(
|
||||
(recipient) => recipient.signingStatus === SigningStatus.REJECTED,
|
||||
@ -88,21 +113,12 @@ export const sealDocument = async ({
|
||||
!isRejected &&
|
||||
recipients.some((recipient) => recipient.signingStatus !== SigningStatus.SIGNED)
|
||||
) {
|
||||
throw new Error(`Document ${document.id} has unsigned recipients`);
|
||||
throw new Error(`Envelope ${envelope.id} has unsigned recipients`);
|
||||
}
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
},
|
||||
include: {
|
||||
signature: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Skip the field check if the document is rejected
|
||||
if (!isRejected && fieldsContainUnsignedRequiredField(fields)) {
|
||||
throw new Error(`Document ${document.id} has unsigned required fields`);
|
||||
throw new Error(`Document ${envelope.id} has unsigned required fields`);
|
||||
}
|
||||
|
||||
if (isResealing) {
|
||||
@ -116,8 +132,8 @@ export const sealDocument = async ({
|
||||
|
||||
const certificateData = settings.includeSigningCertificate
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
documentId: legacyDocumentId,
|
||||
language: envelope.documentMeta.language,
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get certificate PDF');
|
||||
console.error(e);
|
||||
@ -128,8 +144,8 @@ export const sealDocument = async ({
|
||||
|
||||
const auditLogData = settings.includeAuditLog
|
||||
? await getAuditLogsPdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
documentId: legacyDocumentId,
|
||||
language: envelope.documentMeta.language,
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get audit logs PDF');
|
||||
console.error(e);
|
||||
@ -171,7 +187,7 @@ export const sealDocument = async ({
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
document.useLegacyFieldInsertion
|
||||
envelope.useLegacyFieldInsertion
|
||||
? await legacy_insertFieldInPDF(doc, field)
|
||||
: await insertFieldInPDF(doc, field);
|
||||
}
|
||||
@ -183,7 +199,8 @@ export const sealDocument = async ({
|
||||
|
||||
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
|
||||
|
||||
const { name } = path.parse(document.title);
|
||||
// Todo: Envelopes use EnvelopeItem title instead.
|
||||
const { name } = path.parse(envelope.title);
|
||||
|
||||
// Add suffix based on document status
|
||||
const suffix = isRejected ? '_rejected.pdf' : '_signed.pdf';
|
||||
@ -201,16 +218,16 @@ export const sealDocument = async ({
|
||||
distinctId: nanoid(),
|
||||
event: 'App: Document Sealed',
|
||||
properties: {
|
||||
documentId: document.id,
|
||||
documentId: envelope.id,
|
||||
isRejected,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.document.update({
|
||||
await tx.envelope.update({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
},
|
||||
data: {
|
||||
status: isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED,
|
||||
@ -230,7 +247,7 @@ export const sealDocument = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
requestMetadata,
|
||||
user: null,
|
||||
data: {
|
||||
@ -242,15 +259,14 @@ export const sealDocument = async ({
|
||||
});
|
||||
|
||||
if (sendEmail && !isResealing) {
|
||||
await sendCompletedEmail({ documentId, requestMetadata });
|
||||
await sendCompletedEmail({ id, requestMetadata });
|
||||
}
|
||||
|
||||
const updatedDocument = await prisma.document.findFirstOrThrow({
|
||||
const updatedDocument = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
@ -260,8 +276,8 @@ export const sealDocument = async ({
|
||||
event: isRejected
|
||||
? WebhookTriggerEvents.DOCUMENT_REJECTED
|
||||
: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId ?? undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Document, Recipient, User } from '@prisma/client';
|
||||
import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
import type { Envelope, Recipient, User } from '@prisma/client';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import {
|
||||
@ -9,6 +10,8 @@ import {
|
||||
} from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||
|
||||
export type SearchDocumentsWithKeywordOptions = {
|
||||
query: string;
|
||||
userId: number;
|
||||
@ -26,8 +29,9 @@ export const searchDocumentsWithKeyword = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const documents = await prisma.document.findMany({
|
||||
const envelopes = await prisma.envelope.findMany({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
OR: [
|
||||
{
|
||||
title: {
|
||||
@ -128,26 +132,26 @@ export const searchDocumentsWithKeyword = async ({
|
||||
take: limit,
|
||||
});
|
||||
|
||||
const isOwner = (document: Document, user: User) => document.userId === user.id;
|
||||
const isOwner = (envelope: Envelope, user: User) => envelope.userId === user.id;
|
||||
|
||||
const getSigningLink = (recipients: Recipient[], user: User) =>
|
||||
`/sign/${recipients.find((r) => r.email === user.email)?.token}`;
|
||||
|
||||
const maskedDocuments = documents
|
||||
.filter((document) => {
|
||||
if (!document.teamId || isOwner(document, user)) {
|
||||
const maskedDocuments = envelopes
|
||||
.filter((envelope) => {
|
||||
if (!envelope.teamId || isOwner(envelope, user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const teamMemberRole = getHighestTeamRoleInGroup(
|
||||
document.team.teamGroups.filter((tg) => tg.teamId === document.teamId),
|
||||
envelope.team.teamGroups.filter((tg) => tg.teamId === envelope.teamId),
|
||||
);
|
||||
|
||||
if (!teamMemberRole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const canAccessDocument = match([document.visibility, teamMemberRole])
|
||||
const canAccessDocument = match([envelope.visibility, teamMemberRole])
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true)
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true)
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MEMBER], () => true)
|
||||
@ -158,23 +162,29 @@ export const searchDocumentsWithKeyword = async ({
|
||||
|
||||
return canAccessDocument;
|
||||
})
|
||||
.map((document) => {
|
||||
const { recipients, ...documentWithoutRecipient } = document;
|
||||
.map((envelope) => {
|
||||
const { recipients, ...documentWithoutRecipient } = envelope;
|
||||
|
||||
let documentPath;
|
||||
|
||||
if (isOwner(document, user)) {
|
||||
documentPath = `${formatDocumentsPath(document.team?.url)}/${document.id}`;
|
||||
} else if (document.teamId && document.team.teamGroups.length > 0) {
|
||||
documentPath = `${formatDocumentsPath(document.team.url)}/${document.id}`;
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
if (isOwner(envelope, user)) {
|
||||
documentPath = `${formatDocumentsPath(envelope.team.url)}/${legacyDocumentId}`;
|
||||
} else if (envelope.teamId && envelope.team.teamGroups.length > 0) {
|
||||
documentPath = `${formatDocumentsPath(envelope.team.url)}/${legacyDocumentId}`;
|
||||
} else {
|
||||
documentPath = getSigningLink(recipients, user);
|
||||
}
|
||||
|
||||
return {
|
||||
...documentWithoutRecipient,
|
||||
team: {
|
||||
id: envelope.teamId,
|
||||
url: envelope.team.url,
|
||||
},
|
||||
path: documentPath,
|
||||
value: [document.id, document.title, ...document.recipients.map((r) => r.email)].join(' '),
|
||||
value: [envelope.id, envelope.title, ...envelope.recipients.map((r) => r.email)].join(' '),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DocumentSource } from '@prisma/client';
|
||||
import { DocumentSource, EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
|
||||
@ -14,23 +14,33 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email'
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { formatDocumentsPath } from '../../utils/teams';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
export interface SendDocumentOptions {
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
requestMetadata?: RequestMetadata;
|
||||
}
|
||||
|
||||
export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDocumentOptions) => {
|
||||
const document = await prisma.document.findUnique({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOptions) => {
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
||||
include: {
|
||||
documentData: true,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: {
|
||||
select: {
|
||||
type: true,
|
||||
id: true,
|
||||
data: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
user: {
|
||||
@ -49,13 +59,13 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
const isDirectTemplate = document?.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
||||
const isDirectTemplate = envelope?.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
if (envelope.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
@ -63,28 +73,37 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const { user: owner } = document;
|
||||
const { user: owner } = envelope;
|
||||
|
||||
const completedDocument = await getFileServerSide(document.documentData);
|
||||
const completedDocumentEmailAttachments = await Promise.all(
|
||||
envelope.envelopeItems.map(async (document) => {
|
||||
const file = await getFileServerSide(document.documentData);
|
||||
|
||||
return {
|
||||
fileName: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
|
||||
content: Buffer.from(file),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
let documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(
|
||||
document.team?.url,
|
||||
)}/${document.id}`;
|
||||
envelope.team?.url,
|
||||
)}/${envelope.id}`;
|
||||
|
||||
if (document.team?.url) {
|
||||
documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${document.team.url}/documents/${
|
||||
document.id
|
||||
if (envelope.team?.url) {
|
||||
documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${envelope.team.url}/documents/${
|
||||
envelope.id
|
||||
}`;
|
||||
}
|
||||
|
||||
const emailSettings = extractDerivedDocumentEmailSettings(document.documentMeta);
|
||||
const emailSettings = extractDerivedDocumentEmailSettings(envelope.documentMeta);
|
||||
const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted;
|
||||
const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted;
|
||||
|
||||
@ -95,11 +114,11 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
// - Recipient emails are disabled
|
||||
if (
|
||||
isOwnerDocumentCompletedEmailEnabled &&
|
||||
(!document.recipients.find((recipient) => recipient.email === owner.email) ||
|
||||
(!envelope.recipients.find((recipient) => recipient.email === owner.email) ||
|
||||
!isDocumentCompletedEmailEnabled)
|
||||
) {
|
||||
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
assetBaseUrl,
|
||||
downloadLink: documentOwnerDownloadLink,
|
||||
});
|
||||
@ -127,18 +146,13 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
subject: i18n._(msg`Signing Complete!`),
|
||||
html,
|
||||
text,
|
||||
attachments: [
|
||||
{
|
||||
filename: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
|
||||
content: Buffer.from(completedDocument),
|
||||
},
|
||||
],
|
||||
attachments: completedDocumentEmailAttachments,
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
user: null,
|
||||
requestMetadata,
|
||||
data: {
|
||||
@ -158,22 +172,22 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
document.recipients.map(async (recipient) => {
|
||||
envelope.recipients.map(async (recipient) => {
|
||||
const customEmailTemplate = {
|
||||
'signer.name': recipient.name,
|
||||
'signer.email': recipient.email,
|
||||
'document.name': document.title,
|
||||
'document.name': envelope.title,
|
||||
};
|
||||
|
||||
const downloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}/complete`;
|
||||
|
||||
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
assetBaseUrl,
|
||||
downloadLink: recipient.email === owner.email ? documentOwnerDownloadLink : downloadLink,
|
||||
customBody:
|
||||
isDirectTemplate && document.documentMeta?.message
|
||||
? renderCustomEmailTemplate(document.documentMeta.message, customEmailTemplate)
|
||||
isDirectTemplate && envelope.documentMeta?.message
|
||||
? renderCustomEmailTemplate(envelope.documentMeta.message, customEmailTemplate)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
@ -198,23 +212,18 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject:
|
||||
isDirectTemplate && document.documentMeta?.subject
|
||||
? renderCustomEmailTemplate(document.documentMeta.subject, customEmailTemplate)
|
||||
isDirectTemplate && envelope.documentMeta?.subject
|
||||
? renderCustomEmailTemplate(envelope.documentMeta.subject, customEmailTemplate)
|
||||
: i18n._(msg`Signing Complete!`),
|
||||
html,
|
||||
text,
|
||||
attachments: [
|
||||
{
|
||||
filename: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
|
||||
content: Buffer.from(completedDocument),
|
||||
},
|
||||
],
|
||||
attachments: completedDocumentEmailAttachments,
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
user: null,
|
||||
requestMetadata,
|
||||
data: {
|
||||
|
||||
@ -14,14 +14,15 @@ import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
export interface SendDeleteEmailOptions {
|
||||
documentId: number;
|
||||
envelopeId: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOptions) => {
|
||||
const document = await prisma.document.findFirst({
|
||||
// Note: Currently only sent by Admin function
|
||||
export const sendDeleteEmail = async ({ envelopeId, reason }: SendDeleteEmailOptions) => {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
id: envelopeId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
@ -35,14 +36,14 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).documentDeleted;
|
||||
|
||||
if (!isDocumentDeletedEmailEnabled) {
|
||||
@ -53,17 +54,17 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
emailType: 'INTERNAL',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const { email, name } = document.user;
|
||||
const { email, name } = envelope.user;
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(DocumentSuperDeleteEmailTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
reason,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
@ -16,17 +17,18 @@ import { jobs } from '../../jobs/client';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export type SendDocumentOptions = {
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
sendEmail?: boolean;
|
||||
@ -34,75 +36,92 @@ export type SendDocumentOptions = {
|
||||
};
|
||||
|
||||
export const sendDocument = async ({
|
||||
documentId,
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
sendEmail,
|
||||
requestMetadata,
|
||||
}: SendDocumentOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
||||
},
|
||||
documentMeta: true,
|
||||
documentData: true,
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
documentData: {
|
||||
select: {
|
||||
type: true,
|
||||
id: true,
|
||||
data: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
if (envelope.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (isDocumentCompleted(envelope.status)) {
|
||||
throw new Error('Can not send completed document');
|
||||
}
|
||||
|
||||
const signingOrder = document.documentMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
let recipientsToNotify = document.recipients;
|
||||
const signingOrder = envelope.documentMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
|
||||
|
||||
let recipientsToNotify = envelope.recipients;
|
||||
|
||||
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
// Get the currently active recipient.
|
||||
recipientsToNotify = document.recipients
|
||||
recipientsToNotify = envelope.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.
|
||||
// received the envelope.
|
||||
recipientsToNotify.filter((r) => r.sendStatus !== SendStatus.SENT);
|
||||
}
|
||||
|
||||
const { documentData } = document;
|
||||
const envelopeItem = envelope.envelopeItems[0];
|
||||
const documentData = envelopeItem?.documentData;
|
||||
|
||||
if (!documentData.data) {
|
||||
throw new Error('Document data not found');
|
||||
// Todo: Envelopes
|
||||
if (!envelopeItem || !documentData || envelope.envelopeItems.length !== 1) {
|
||||
throw new Error('Invalid document data');
|
||||
}
|
||||
|
||||
if (document.formValues) {
|
||||
// Todo: Envelopes need to support multiple envelope items.
|
||||
if (envelope.formValues) {
|
||||
const file = await getFileServerSide(documentData);
|
||||
|
||||
const prefilled = await insertFormValuesInPdf({
|
||||
pdf: Buffer.from(file),
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
formValues: document.formValues as Record<string, string | number | boolean>,
|
||||
formValues: envelope.formValues as Record<string, string | number | boolean>,
|
||||
});
|
||||
|
||||
let fileName = document.title;
|
||||
let fileName = envelope.title;
|
||||
|
||||
if (!document.title.endsWith('.pdf')) {
|
||||
fileName = `${document.title}.pdf`;
|
||||
if (!envelope.title.endsWith('.pdf')) {
|
||||
fileName = `${envelope.title}.pdf`;
|
||||
}
|
||||
|
||||
const newDocumentData = await putPdfFileServerSide({
|
||||
@ -111,9 +130,9 @@ export const sendDocument = async ({
|
||||
arrayBuffer: async () => Promise.resolve(prefilled),
|
||||
});
|
||||
|
||||
const result = await prisma.document.update({
|
||||
const result = await prisma.envelopeItem.update({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelopeItem.id,
|
||||
},
|
||||
data: {
|
||||
documentDataId: newDocumentData.id,
|
||||
@ -133,7 +152,7 @@ export const sendDocument = async ({
|
||||
// const fieldsWithSignerEmail = fields.map((field) => ({
|
||||
// ...field,
|
||||
// signerEmail:
|
||||
// document.Recipient.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
||||
// envelope.Recipient.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
||||
// }));
|
||||
|
||||
// const everySignerHasSignature = document?.Recipient.every(
|
||||
@ -148,7 +167,7 @@ export const sendDocument = async ({
|
||||
// throw new Error('Some signers have not been assigned a signature field.');
|
||||
// }
|
||||
|
||||
const allRecipientsHaveNoActionToTake = document.recipients.every(
|
||||
const allRecipientsHaveNoActionToTake = envelope.recipients.every(
|
||||
(recipient) =>
|
||||
recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
|
||||
);
|
||||
@ -157,15 +176,15 @@ export const sendDocument = async ({
|
||||
await jobs.triggerJob({
|
||||
name: 'internal.seal-document',
|
||||
payload: {
|
||||
documentId,
|
||||
documentId: legacyDocumentId,
|
||||
requestMetadata: requestMetadata?.requestMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
// Keep the return type the same for the `sendDocument` method
|
||||
return await prisma.document.findFirstOrThrow({
|
||||
return await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
id: envelope.id,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
@ -174,21 +193,21 @@ export const sendDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const updatedDocument = await prisma.$transaction(async (tx) => {
|
||||
if (document.status === DocumentStatus.DRAFT) {
|
||||
const updatedEnvelope = await prisma.$transaction(async (tx) => {
|
||||
if (envelope.status === DocumentStatus.DRAFT) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return await tx.document.update({
|
||||
return await tx.envelope.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
id: envelope.id,
|
||||
},
|
||||
data: {
|
||||
status: DocumentStatus.PENDING,
|
||||
@ -201,7 +220,7 @@ export const sendDocument = async ({
|
||||
});
|
||||
|
||||
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).recipientSigningRequest;
|
||||
|
||||
// Only send email if one of the following is true:
|
||||
@ -218,7 +237,7 @@ export const sendDocument = async ({
|
||||
name: 'send.signing.requested.email',
|
||||
payload: {
|
||||
userId,
|
||||
documentId,
|
||||
documentId: legacyDocumentId,
|
||||
recipientId: recipient.id,
|
||||
requestMetadata: requestMetadata?.requestMetadata,
|
||||
},
|
||||
@ -229,10 +248,10 @@ export const sendDocument = async ({
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_SENT,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedEnvelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return updatedDocument;
|
||||
return updatedEnvelope;
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending';
|
||||
@ -9,18 +10,20 @@ import { prisma } from '@documenso/prisma';
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
export interface SendPendingEmailOptions {
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
recipientId: number;
|
||||
}
|
||||
|
||||
export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingEmailOptions) => {
|
||||
const document = await prisma.document.findFirst({
|
||||
export const sendPendingEmail = async ({ id, recipientId }: SendPendingEmailOptions) => {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
@ -37,11 +40,11 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
if (envelope.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
@ -49,27 +52,27 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).documentPending;
|
||||
|
||||
if (!isDocumentPendingEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [recipient] = document.recipients;
|
||||
const [recipient] = envelope.recipients;
|
||||
|
||||
const { email, name } = recipient;
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(DocumentPendingEmailTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import type { DocumentVisibility, Prisma } from '@prisma/client';
|
||||
import { EnvelopeType, FolderType } from '@prisma/client';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
@ -9,10 +9,12 @@ import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/do
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type UpdateDocumentOptions = {
|
||||
userId: number;
|
||||
@ -25,6 +27,7 @@ export type UpdateDocumentOptions = {
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
useLegacyFieldInsertion?: boolean;
|
||||
folderId?: string | null;
|
||||
};
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
@ -36,14 +39,18 @@ export const updateDocument = async ({
|
||||
data,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentOptions) => {
|
||||
const { documentWhereInput, team } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput, team } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
@ -57,57 +64,70 @@ export const updateDocument = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isDocumentOwner = document.userId === userId;
|
||||
const requestedVisibility = data?.visibility;
|
||||
const isEnvelopeOwner = envelope.userId === userId;
|
||||
|
||||
if (!isDocumentOwner) {
|
||||
match(team.currentTeamRole)
|
||||
.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',
|
||||
});
|
||||
});
|
||||
// Validate whether the new visibility setting is allowed for the current user.
|
||||
if (
|
||||
!isEnvelopeOwner &&
|
||||
data?.visibility &&
|
||||
!canAccessTeamDocument(team.currentTeamRole, data.visibility)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
return envelope;
|
||||
}
|
||||
|
||||
let folderUpdateQuery: Prisma.FolderUpdateOneWithoutEnvelopesNestedInput | undefined = undefined;
|
||||
|
||||
// Validate folder ID.
|
||||
if (data.folderId) {
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: data.folderId,
|
||||
team: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
}),
|
||||
type: FolderType.DOCUMENT,
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
|
||||
folderUpdateQuery = {
|
||||
connect: {
|
||||
id: data.folderId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Move to root folder if folderId is null.
|
||||
if (data.folderId === null) {
|
||||
folderUpdateQuery = {
|
||||
disconnect: true,
|
||||
};
|
||||
}
|
||||
|
||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
documentAuth: envelope.authOptions,
|
||||
});
|
||||
|
||||
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
|
||||
@ -120,14 +140,14 @@ export const updateDocument = async ({
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth.length > 0 && !document.team.organisation.organisationClaim.flags.cfr21) {
|
||||
if (newGlobalActionAuth.length > 0 && !envelope.team.organisation.organisationClaim.flags.cfr21) {
|
||||
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 isTitleSame = data.title === undefined || data.title === envelope.title;
|
||||
const isExternalIdSame = data.externalId === undefined || data.externalId === envelope.externalId;
|
||||
const isGlobalAccessSame =
|
||||
documentGlobalAccessAuth === undefined ||
|
||||
isDeepEqual(documentGlobalAccessAuth, newGlobalAccessAuth);
|
||||
@ -135,11 +155,12 @@ export const updateDocument = async ({
|
||||
documentGlobalActionAuth === undefined ||
|
||||
isDeepEqual(documentGlobalActionAuth, newGlobalActionAuth);
|
||||
const isDocumentVisibilitySame =
|
||||
data.visibility === undefined || data.visibility === document.visibility;
|
||||
data.visibility === undefined || data.visibility === envelope.visibility;
|
||||
const isFolderSame = data.folderId === undefined || data.folderId === envelope.folderId;
|
||||
|
||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||
|
||||
if (!isTitleSame && document.status !== DocumentStatus.DRAFT) {
|
||||
if (!isTitleSame && envelope.status !== DocumentStatus.DRAFT) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'You cannot update the title if the document has been sent',
|
||||
});
|
||||
@ -149,10 +170,10 @@ export const updateDocument = async ({
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: document.title,
|
||||
from: envelope.title,
|
||||
to: data.title || '',
|
||||
},
|
||||
}),
|
||||
@ -163,10 +184,10 @@ export const updateDocument = async ({
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: document.externalId,
|
||||
from: envelope.externalId,
|
||||
to: data.externalId || '',
|
||||
},
|
||||
}),
|
||||
@ -177,7 +198,7 @@ export const updateDocument = async ({
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalAccessAuth,
|
||||
@ -191,7 +212,7 @@ export const updateDocument = async ({
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalActionAuth,
|
||||
@ -205,19 +226,34 @@ export const updateDocument = async ({
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: document.visibility,
|
||||
from: envelope.visibility,
|
||||
to: data.visibility || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Todo: Decide if we want to log moving the document around.
|
||||
// if (!isFolderSame) {
|
||||
// auditLogs.push(
|
||||
// createDocumentAuditLogData({
|
||||
// type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FOLDER_UPDATED,
|
||||
// envelopeId: envelope.id,
|
||||
// metadata: requestMetadata,
|
||||
// data: {
|
||||
// from: envelope.folderId,
|
||||
// to: data.folderId || '',
|
||||
// },
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
// Early return if nothing is required.
|
||||
if (auditLogs.length === 0 && data.useLegacyFieldInsertion === undefined) {
|
||||
return document;
|
||||
if (auditLogs.length === 0 && data.useLegacyFieldInsertion === undefined && isFolderSame) {
|
||||
return envelope;
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
@ -226,9 +262,10 @@ export const updateDocument = async ({
|
||||
globalActionAuth: newGlobalActionAuth,
|
||||
});
|
||||
|
||||
const updatedDocument = await tx.document.update({
|
||||
const updatedDocument = await tx.envelope.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
id: envelope.id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
data: {
|
||||
title: data.title,
|
||||
@ -236,6 +273,7 @@ export const updateDocument = async ({
|
||||
visibility: data.visibility as DocumentVisibility,
|
||||
useLegacyFieldInsertion: data.useLegacyFieldInsertion,
|
||||
authOptions,
|
||||
folder: folderUpdateQuery,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Document, Field, Recipient } from '@prisma/client';
|
||||
import type { Envelope, Field, Recipient } from '@prisma/client';
|
||||
import { FieldType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
@ -6,8 +6,8 @@ import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
import { isRecipientAuthorized } from './is-recipient-authorized';
|
||||
|
||||
export type ValidateFieldAuthOptions = {
|
||||
documentAuthOptions: Document['authOptions'];
|
||||
recipient: Pick<Recipient, 'authOptions' | 'email' | 'documentId'>;
|
||||
documentAuthOptions: Envelope['authOptions'];
|
||||
recipient: Pick<Recipient, 'authOptions' | 'email'>;
|
||||
field: Field;
|
||||
userId?: number;
|
||||
authOptions?: TRecipientActionAuth;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ReadStatus, SendStatus } from '@prisma/client';
|
||||
import { EnvelopeType, ReadStatus, SendStatus } from '@prisma/client';
|
||||
import { WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
@ -9,7 +9,7 @@ import { prisma } from '@documenso/prisma';
|
||||
import type { TDocumentAccessAuthTypes } from '../../types/document-auth';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
@ -27,19 +27,30 @@ export const viewedDocument = async ({
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
token,
|
||||
envelope: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
envelope: {
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient || !recipient.documentId) {
|
||||
if (!recipient) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { documentId } = recipient;
|
||||
const { envelope } = recipient;
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
@ -75,7 +86,7 @@ export const viewedDocument = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
@ -92,24 +103,10 @@ export const viewedDocument = async ({
|
||||
});
|
||||
});
|
||||
|
||||
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: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(document)),
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { DocumentMeta, DocumentVisibility } from '@prisma/client';
|
||||
import type { DocumentMeta, DocumentVisibility, TemplateType } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
EnvelopeType,
|
||||
FolderType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
@ -21,47 +22,68 @@ import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../t
|
||||
import type { TDocumentFormValues } from '../../types/document-form-values';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getMemberRoles } from '../team/get-member-roles';
|
||||
import { incrementDocumentId, incrementTemplateId } from '../envelope/increment-id';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentOptions = {
|
||||
export type CreateEnvelopeOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentDataId: string;
|
||||
normalizePdf?: boolean;
|
||||
data: {
|
||||
type: EnvelopeType;
|
||||
title: string;
|
||||
externalId?: string;
|
||||
envelopeItems: { title?: string; documentDataId: string }[];
|
||||
formValues?: TDocumentFormValues;
|
||||
|
||||
timezone?: string;
|
||||
userTimezone?: string;
|
||||
|
||||
templateType?: TemplateType;
|
||||
publicTitle?: string;
|
||||
publicDescription?: string;
|
||||
|
||||
visibility?: DocumentVisibility;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
formValues?: TDocumentFormValues;
|
||||
recipients: TCreateDocumentTemporaryRequest['recipients'];
|
||||
recipients?: TCreateDocumentTemporaryRequest['recipients'];
|
||||
folderId?: string;
|
||||
};
|
||||
meta?: Partial<Omit<DocumentMeta, 'id' | 'templateId'>>;
|
||||
meta?: Partial<Omit<DocumentMeta, 'id'>>;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const createDocumentV2 = async ({
|
||||
export const createEnvelope = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentDataId,
|
||||
normalizePdf,
|
||||
data,
|
||||
meta,
|
||||
requestMetadata,
|
||||
}: CreateDocumentOptions) => {
|
||||
const { title, formValues, folderId } = data;
|
||||
}: CreateEnvelopeOptions) => {
|
||||
const {
|
||||
type,
|
||||
title,
|
||||
externalId,
|
||||
formValues,
|
||||
timezone,
|
||||
userTimezone,
|
||||
folderId,
|
||||
templateType,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
publicTitle,
|
||||
publicDescription,
|
||||
visibility: visibilityOverride,
|
||||
} = data;
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({ teamId, userId }),
|
||||
@ -80,11 +102,12 @@ export const createDocumentV2 = async ({
|
||||
});
|
||||
}
|
||||
|
||||
// Verify that the folder exists and is associated with the team.
|
||||
if (folderId) {
|
||||
const folder = await prisma.folder.findUnique({
|
||||
where: {
|
||||
id: folderId,
|
||||
type: FolderType.DOCUMENT,
|
||||
type: data.type === EnvelopeType.TEMPLATE ? FolderType.TEMPLATE : FolderType.DOCUMENT,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
});
|
||||
@ -101,32 +124,44 @@ export const createDocumentV2 = async ({
|
||||
teamId,
|
||||
});
|
||||
|
||||
let envelopeItems: { title?: string; documentDataId: string }[] = data.envelopeItems;
|
||||
|
||||
if (normalizePdf) {
|
||||
const documentData = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
id: documentDataId,
|
||||
},
|
||||
});
|
||||
envelopeItems = await Promise.all(
|
||||
data.envelopeItems.map(async (item) => {
|
||||
const documentData = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
id: item.documentDataId,
|
||||
},
|
||||
});
|
||||
|
||||
if (documentData) {
|
||||
const buffer = await getFileServerSide(documentData);
|
||||
if (!documentData) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document data not found',
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer));
|
||||
const buffer = await getFileServerSide(documentData);
|
||||
|
||||
const newDocumentData = await putPdfFileServerSide({
|
||||
name: title.endsWith('.pdf') ? title : `${title}.pdf`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(normalizedPdf),
|
||||
});
|
||||
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer));
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
documentDataId = newDocumentData.id;
|
||||
}
|
||||
const newDocumentData = await putPdfFileServerSide({
|
||||
name: title.endsWith('.pdf') ? title : `${title}.pdf`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(normalizedPdf),
|
||||
});
|
||||
|
||||
return {
|
||||
title: item.title,
|
||||
documentDataId: newDocumentData.id,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: data?.globalAccessAuth || [],
|
||||
globalActionAuth: data?.globalActionAuth || [],
|
||||
globalAccessAuth: globalAccessAuth || [],
|
||||
globalActionAuth: globalActionAuth || [],
|
||||
});
|
||||
|
||||
const recipientsHaveActionAuth = data.recipients?.some(
|
||||
@ -143,15 +178,7 @@ export const createDocumentV2 = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const { teamRole } = await getMemberRoles({
|
||||
teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const visibility = determineDocumentVisibility(settings.documentVisibility, teamRole);
|
||||
const visibility = visibilityOverride || settings.documentVisibility;
|
||||
|
||||
const emailId = meta?.emailId;
|
||||
|
||||
@ -171,26 +198,62 @@ export const createDocumentV2 = async ({
|
||||
}
|
||||
}
|
||||
|
||||
// userTimezone is last because it's always passed in regardless of the organisation/team settings
|
||||
// for uploads from the frontend
|
||||
const timezoneToUse = timezone || settings.documentTimezone || userTimezone;
|
||||
|
||||
const documentMeta = await prisma.documentMeta.create({
|
||||
data: extractDerivedDocumentMeta(settings, {
|
||||
...meta,
|
||||
timezone: timezoneToUse,
|
||||
}),
|
||||
});
|
||||
|
||||
const secondaryId =
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? await incrementDocumentId().then((v) => v.formattedDocumentId)
|
||||
: await incrementTemplateId().then((v) => v.formattedTemplateId);
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.create({
|
||||
const envelope = await tx.envelope.create({
|
||||
data: {
|
||||
id: prefixedId('envelope'),
|
||||
secondaryId,
|
||||
type,
|
||||
title,
|
||||
qrToken: prefixedId('qr'),
|
||||
externalId: data.externalId,
|
||||
documentDataId,
|
||||
externalId,
|
||||
envelopeItems: {
|
||||
createMany: {
|
||||
data: envelopeItems.map((item) => ({
|
||||
id: prefixedId('envelope_item'),
|
||||
title: item.title || title,
|
||||
documentDataId: item.documentDataId,
|
||||
})),
|
||||
},
|
||||
},
|
||||
userId,
|
||||
teamId,
|
||||
authOptions,
|
||||
visibility,
|
||||
folderId,
|
||||
formValues,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
create: extractDerivedDocumentMeta(settings, meta),
|
||||
},
|
||||
source: DocumentSource.DOCUMENT, // Todo: Migration
|
||||
documentMetaId: documentMeta.id,
|
||||
|
||||
// Template specific fields.
|
||||
templateType: type === EnvelopeType.TEMPLATE ? templateType : undefined,
|
||||
publicTitle: type === EnvelopeType.TEMPLATE ? publicTitle : undefined,
|
||||
publicDescription: type === EnvelopeType.TEMPLATE ? publicDescription : undefined,
|
||||
},
|
||||
include: {
|
||||
envelopeItems: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Todo: Envelopes - Support multiple envelope items.
|
||||
const firstEnvelopeItemId = envelope.envelopeItems[0].id;
|
||||
|
||||
await Promise.all(
|
||||
(data.recipients || []).map(async (recipient) => {
|
||||
const recipientAuthOptions = createRecipientAuthOptions({
|
||||
@ -200,7 +263,7 @@ export const createDocumentV2 = async ({
|
||||
|
||||
await tx.recipient.create({
|
||||
data: {
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
@ -213,7 +276,8 @@ export const createDocumentV2 = async ({
|
||||
fields: {
|
||||
createMany: {
|
||||
data: (recipient.fields || []).map((field) => ({
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
envelopeItemId: firstEnvelopeItemId,
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
@ -231,48 +295,49 @@ export const createDocumentV2 = async ({
|
||||
}),
|
||||
);
|
||||
|
||||
// Todo: Is it necessary to create a full audit logs 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({
|
||||
const createdEnvelope = await tx.envelope.findFirst({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
fields: true,
|
||||
folder: true,
|
||||
envelopeItems: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdDocument) {
|
||||
if (!createdEnvelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
// Only create audit logs and webhook events for documents.
|
||||
if (type === EnvelopeType.DOCUMENT) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
title,
|
||||
source: {
|
||||
type: DocumentSource.DOCUMENT,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return createdDocument;
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
|
||||
return createdEnvelope;
|
||||
});
|
||||
};
|
||||
168
packages/lib/server-only/envelope/get-envelope-by-id.ts
Normal file
168
packages/lib/server-only/envelope/get-envelope-by-id.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import type { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type GetEnvelopeByIdOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
|
||||
/**
|
||||
* The validated team ID.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The unvalidated team ID.
|
||||
*/
|
||||
teamId: number;
|
||||
|
||||
/**
|
||||
* The type of envelope to get.
|
||||
*
|
||||
* Set to null to bypass check.
|
||||
*/
|
||||
type: EnvelopeType | null;
|
||||
};
|
||||
|
||||
export const getEnvelopeById = async ({ id, userId, teamId, type }: GetEnvelopeByIdOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
folder: true,
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
fields: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope could not be found',
|
||||
});
|
||||
}
|
||||
|
||||
return envelope;
|
||||
};
|
||||
|
||||
export type GetEnvelopeByIdResponse = Awaited<ReturnType<typeof getEnvelopeById>>;
|
||||
|
||||
export type GetEnvelopeWhereInputOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
|
||||
/**
|
||||
* The user ID who has been authenticated.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The unknown teamId from the request.
|
||||
*/
|
||||
teamId: number;
|
||||
|
||||
/**
|
||||
* The type of envelope to get.
|
||||
*
|
||||
* Set to null to bypass check.
|
||||
*/
|
||||
type: EnvelopeType | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the where input for a given Prisma envelope query.
|
||||
*
|
||||
* This will return a query that allows a user to get a document if they have valid access to it.
|
||||
*
|
||||
* NOTE: Be extremely careful when modifying this function. Needs at minimum two reviewers to approve any changes.
|
||||
*/
|
||||
export const getEnvelopeWhereInput = async ({
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
}: GetEnvelopeWhereInputOptions) => {
|
||||
// Validate that the user belongs to the team provided.
|
||||
const team = await getTeamById({ teamId, userId });
|
||||
|
||||
const envelopeOrInput: Prisma.EnvelopeWhereInput[] = [
|
||||
// Allow access if they own the document.
|
||||
{
|
||||
userId,
|
||||
},
|
||||
// Or, if they belong to the team that the document is associated with.
|
||||
{
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||
},
|
||||
teamId: team.id,
|
||||
},
|
||||
];
|
||||
|
||||
// Allow access to documents sent from the team email.
|
||||
if (team.teamEmail) {
|
||||
envelopeOrInput.push({
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
||||
// NOTE: DO NOT PUT ANY CODE AFTER THIS POINT.
|
||||
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
||||
|
||||
const envelopeWhereInput: Prisma.EnvelopeWhereUniqueInput = {
|
||||
...unsafeBuildEnvelopeIdQuery(id, type),
|
||||
OR: envelopeOrInput,
|
||||
};
|
||||
|
||||
// Final backup validation incase something goes wrong.
|
||||
if (
|
||||
!envelopeWhereInput.OR ||
|
||||
envelopeWhereInput.OR.length < 2 ||
|
||||
!userId ||
|
||||
!teamId ||
|
||||
!team.id ||
|
||||
teamId !== team.id
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Query not valid',
|
||||
});
|
||||
}
|
||||
|
||||
// Do not modify this return directly, all adjustments need to be made prior to the above if statement.
|
||||
return {
|
||||
envelopeWhereInput,
|
||||
team,
|
||||
};
|
||||
};
|
||||
39
packages/lib/server-only/envelope/increment-id.ts
Normal file
39
packages/lib/server-only/envelope/increment-id.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { mapDocumentIdToSecondaryId, mapTemplateIdToSecondaryId } from '../../utils/envelope';
|
||||
|
||||
export const incrementDocumentId = async () => {
|
||||
const documentIdCounter = await prisma.counter.update({
|
||||
where: {
|
||||
id: 'document',
|
||||
},
|
||||
data: {
|
||||
value: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
documentId: documentIdCounter.value,
|
||||
formattedDocumentId: mapDocumentIdToSecondaryId(documentIdCounter.value),
|
||||
};
|
||||
};
|
||||
|
||||
export const incrementTemplateId = async () => {
|
||||
const templateIdCounter = await prisma.counter.update({
|
||||
where: {
|
||||
id: 'template',
|
||||
},
|
||||
data: {
|
||||
value: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
templateId: templateIdCounter.value,
|
||||
formattedTemplateId: mapTemplateIdToSecondaryId(templateIdCounter.value),
|
||||
};
|
||||
};
|
||||
@ -1,126 +0,0 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
|
||||
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 { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
|
||||
export interface CreateDocumentFieldsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
fields: (TFieldAndMeta & {
|
||||
recipientId: number;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
})[];
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const createDocumentFields = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: CreateDocumentFieldsOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
}
|
||||
|
||||
// Field validation.
|
||||
const validatedFields = fields.map((field) => {
|
||||
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Recipient ${field.recipientId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can have new fields created.
|
||||
if (!canRecipientFieldsBeModified(recipient, document.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Recipient type cannot have fields, or they have already interacted with the document.',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
recipientEmail: recipient.email,
|
||||
};
|
||||
});
|
||||
|
||||
const createdFields = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
validatedFields.map(async (field) => {
|
||||
const createdField = await tx.field.create({
|
||||
data: {
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
documentId,
|
||||
recipientId: field.recipientId,
|
||||
},
|
||||
});
|
||||
|
||||
// Handle field created audit log.
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: createdField.secondaryId,
|
||||
fieldRecipientEmail: field.recipientEmail,
|
||||
fieldRecipientId: createdField.recipientId,
|
||||
fieldType: createdField.type,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return createdField;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
fields: createdFields,
|
||||
};
|
||||
};
|
||||
167
packages/lib/server-only/field/create-envelope-fields.ts
Normal file
167
packages/lib/server-only/field/create-envelope-fields.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
|
||||
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 { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface CreateEnvelopeFieldsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
|
||||
fields: (TFieldAndMeta & {
|
||||
/**
|
||||
* The ID of the item to insert the fields into.
|
||||
*
|
||||
* If blank, the first item will be used.
|
||||
*/
|
||||
envelopeItemId?: string;
|
||||
|
||||
recipientId: number;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
})[];
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const createEnvelopeFields = async ({
|
||||
userId,
|
||||
teamId,
|
||||
id,
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: CreateEnvelopeFieldsOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: null, // Null to allow any type of envelope.
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (envelope.type === EnvelopeType.DOCUMENT && envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Envelope already complete',
|
||||
});
|
||||
}
|
||||
|
||||
const firstEnvelopeItem = envelope.envelopeItems[0];
|
||||
|
||||
if (!firstEnvelopeItem) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope item not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Field validation.
|
||||
const validatedFields = fields.map((field) => {
|
||||
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
// The item to attach the fields to MUST belong to the document.
|
||||
if (
|
||||
field.envelopeItemId &&
|
||||
!envelope.envelopeItems.find((envelopeItem) => envelopeItem.id === field.envelopeItemId) // Todo: Migration test this
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Item to attach fields to must belong to the document',
|
||||
});
|
||||
}
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Recipient ${field.recipientId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can have new fields created.
|
||||
if (!canRecipientFieldsBeModified(recipient, envelope.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Recipient type cannot have fields, or they have already interacted with the document.',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
envelopeItemId: field.envelopeItemId || firstEnvelopeItem.id, // Fallback to first envelope item if no envelope item ID is provided.
|
||||
recipientEmail: recipient.email,
|
||||
};
|
||||
});
|
||||
|
||||
const createdFields = await prisma.$transaction(async (tx) => {
|
||||
const newlyCreatedFields = await tx.field.createManyAndReturn({
|
||||
data: validatedFields.map((field) => ({
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
envelopeId: envelope.id,
|
||||
envelopeItemId: field.envelopeItemId,
|
||||
recipientId: field.recipientId,
|
||||
})),
|
||||
});
|
||||
|
||||
// Handle field created audit log.
|
||||
if (envelope.type === EnvelopeType.DOCUMENT) {
|
||||
await tx.documentAuditLog.createMany({
|
||||
data: newlyCreatedFields.map((createdField) => {
|
||||
const recipient = validatedFields.find(
|
||||
(field) => field.recipientId === createdField.recipientId,
|
||||
);
|
||||
|
||||
return createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: createdField.secondaryId,
|
||||
fieldRecipientEmail: recipient?.recipientEmail || '',
|
||||
fieldRecipientId: createdField.recipientId,
|
||||
fieldType: createdField.type,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return newlyCreatedFields;
|
||||
});
|
||||
|
||||
return {
|
||||
fields: createdFields,
|
||||
};
|
||||
};
|
||||
@ -1,136 +0,0 @@
|
||||
import type { FieldType } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZNumberFieldMeta,
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '../../types/field-meta';
|
||||
import type { TFieldMetaSchema as FieldMeta } from '../../types/field-meta';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
|
||||
export type CreateFieldOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
recipientId: number;
|
||||
type: FieldType;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
fieldMeta?: FieldMeta;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const createField = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
recipientId,
|
||||
type,
|
||||
pageNumber,
|
||||
pageX,
|
||||
pageY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
fieldMeta,
|
||||
requestMetadata,
|
||||
}: CreateFieldOptions) => {
|
||||
const { documentWhereInput, team } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(type);
|
||||
|
||||
if (advancedField && !fieldMeta) {
|
||||
throw new Error(
|
||||
'Field meta is required for this type of field. Please provide the appropriate field meta object.',
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMeta && fieldMeta.type.toLowerCase() !== String(type).toLowerCase()) {
|
||||
throw new Error('Field meta type does not match the field type');
|
||||
}
|
||||
|
||||
const result = match(type)
|
||||
.with('RADIO', () => ZRadioFieldMeta.safeParse(fieldMeta))
|
||||
.with('CHECKBOX', () => ZCheckboxFieldMeta.safeParse(fieldMeta))
|
||||
.with('DROPDOWN', () => ZDropdownFieldMeta.safeParse(fieldMeta))
|
||||
.with('NUMBER', () => ZNumberFieldMeta.safeParse(fieldMeta))
|
||||
.with('TEXT', () => ZTextFieldMeta.safeParse(fieldMeta))
|
||||
.with('SIGNATURE', 'INITIALS', 'DATE', 'EMAIL', 'NAME', () => ({
|
||||
success: true,
|
||||
data: {},
|
||||
}))
|
||||
.with('FREE_SIGNATURE', () => ({
|
||||
success: false,
|
||||
error: 'FREE_SIGNATURE is not supported',
|
||||
data: {},
|
||||
}))
|
||||
.exhaustive();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Field meta parsing failed');
|
||||
}
|
||||
|
||||
const field = await prisma.field.create({
|
||||
data: {
|
||||
documentId,
|
||||
recipientId,
|
||||
type,
|
||||
page: pageNumber,
|
||||
positionX: pageX,
|
||||
positionY: pageY,
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: result.data,
|
||||
},
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: 'FIELD_CREATED',
|
||||
documentId,
|
||||
user: {
|
||||
id: team.id,
|
||||
email: team.name,
|
||||
name: '',
|
||||
},
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.recipient?.email ?? '',
|
||||
fieldRecipientId: recipientId,
|
||||
fieldType: field.type,
|
||||
},
|
||||
requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return field;
|
||||
};
|
||||
@ -1,101 +0,0 @@
|
||||
import type { FieldType } from '@prisma/client';
|
||||
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export interface CreateTemplateFieldsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
templateId: number;
|
||||
fields: {
|
||||
recipientId: number;
|
||||
type: FieldType;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
fieldMeta?: TFieldMetaSchema;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const createTemplateFields = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
fields,
|
||||
}: CreateTemplateFieldsOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'template not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Field validation.
|
||||
const validatedFields = fields.map((field) => {
|
||||
const recipient = template.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Recipient ${field.recipientId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can have new fields created.
|
||||
if (!canRecipientFieldsBeModified(recipient, template.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Recipient type cannot have fields, or they have already interacted with the template.',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
recipientEmail: recipient.email,
|
||||
};
|
||||
});
|
||||
|
||||
const createdFields = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
validatedFields.map(async (field) => {
|
||||
const createdField = await tx.field.create({
|
||||
data: {
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
templateId,
|
||||
recipientId: field.recipientId,
|
||||
},
|
||||
});
|
||||
|
||||
return createdField;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
fields: createdFields,
|
||||
};
|
||||
};
|
||||
@ -1,3 +1,5 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
@ -5,7 +7,7 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface DeleteDocumentFieldOptions {
|
||||
userId: number;
|
||||
@ -19,7 +21,8 @@ export const deleteDocumentField = async ({
|
||||
teamId,
|
||||
fieldId,
|
||||
requestMetadata,
|
||||
}: DeleteDocumentFieldOptions): Promise<void> => {
|
||||
}: DeleteDocumentFieldOptions) => {
|
||||
// Unauthenticated check, we do the real check later.
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
@ -32,22 +35,18 @@ export const deleteDocumentField = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const documentId = field.documentId;
|
||||
|
||||
if (!documentId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Field does not belong to a document. Use delete template field instead.',
|
||||
});
|
||||
}
|
||||
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: field.envelopeId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
@ -60,19 +59,19 @@ export const deleteDocumentField = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
}
|
||||
|
||||
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
@ -87,10 +86,11 @@ export const deleteDocumentField = async ({
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const deletedField = await tx.field.delete({
|
||||
where: {
|
||||
id: fieldId,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
});
|
||||
|
||||
@ -98,7 +98,7 @@ export const deleteDocumentField = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: deletedField.secondaryId,
|
||||
@ -108,5 +108,7 @@ export const deleteDocumentField = async ({
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return deletedField;
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
import type { Team } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type DeleteFieldOptions = {
|
||||
fieldId: number;
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const deleteField = async ({
|
||||
fieldId,
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
requestMetadata,
|
||||
}: DeleteFieldOptions) => {
|
||||
const field = await prisma.field.delete({
|
||||
where: {
|
||||
id: fieldId,
|
||||
document: {
|
||||
id: documentId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: 'FIELD_DELETED',
|
||||
documentId,
|
||||
user: {
|
||||
id: team?.id ?? user.id,
|
||||
email: team?.name ?? user.email,
|
||||
name: team ? '' : user.name,
|
||||
},
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.recipient?.email ?? '',
|
||||
fieldRecipientId: field.recipientId ?? -1,
|
||||
fieldType: field.type,
|
||||
},
|
||||
requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return field;
|
||||
};
|
||||
@ -1,7 +1,10 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface DeleteTemplateFieldOptions {
|
||||
userId: number;
|
||||
@ -17,21 +20,34 @@ export const deleteTemplateField = async ({
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
template: {
|
||||
envelope: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!field || !field.templateId) {
|
||||
if (!field) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Field not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Additional validation to check visibility.
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: field.envelopeId,
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
await prisma.field.delete({
|
||||
where: {
|
||||
id: fieldId,
|
||||
id: field.id,
|
||||
envelope: envelopeWhereInput,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { EnvelopeType, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -6,10 +6,12 @@ export type GetCompletedFieldsForTokenOptions = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
// Todo: Envelopes - This needs to be redone since we need to determine which document to show the fields on.
|
||||
export const getCompletedFieldsForToken = async ({ token }: GetCompletedFieldsForTokenOptions) => {
|
||||
return await prisma.field.findMany({
|
||||
where: {
|
||||
document: {
|
||||
envelope: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
|
||||
@ -1,49 +1,56 @@
|
||||
import type { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type GetFieldByIdOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
fieldId: number;
|
||||
envelopeType: EnvelopeType;
|
||||
};
|
||||
|
||||
export const getFieldById = async ({ userId, teamId, fieldId }: GetFieldByIdOptions) => {
|
||||
export const getFieldById = async ({
|
||||
userId,
|
||||
teamId,
|
||||
fieldId,
|
||||
envelopeType,
|
||||
}: GetFieldByIdOptions) => {
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
},
|
||||
include: {
|
||||
document: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
template: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
envelope: {
|
||||
type: envelopeType,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const foundTeamId = field?.document?.teamId || field?.template?.teamId;
|
||||
|
||||
if (!field || !foundTeamId || foundTeamId !== teamId) {
|
||||
if (!field) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Field not found',
|
||||
});
|
||||
}
|
||||
|
||||
const team = await prisma.team.findUnique({
|
||||
where: buildTeamWhereQuery({
|
||||
teamId: foundTeamId,
|
||||
userId,
|
||||
}),
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: field.envelopeId,
|
||||
},
|
||||
type: envelopeType,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
// Additional validation to check visibility.
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Field not found',
|
||||
});
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export interface GetFieldsForDocumentOptions {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
}
|
||||
|
||||
export type DocumentField = Awaited<ReturnType<typeof getFieldsForDocument>>[number];
|
||||
|
||||
export const getFieldsForDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetFieldsForDocumentOptions) => {
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
document: {
|
||||
id: documentId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
signature: true,
|
||||
recipient: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
signingStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
return fields;
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { FieldType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { EnvelopeType, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -6,6 +6,7 @@ export type GetFieldsForTokenOptions = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
// Todo: Envelopes, this will return all fields, might need to filter based on actual documentId.
|
||||
export const getFieldsForToken = async ({ token }: GetFieldsForTokenOptions) => {
|
||||
if (!token) {
|
||||
throw new Error('Missing token');
|
||||
@ -35,7 +36,10 @@ export const getFieldsForToken = async ({ token }: GetFieldsForTokenOptions) =>
|
||||
gte: recipient.signingOrder ?? 0,
|
||||
},
|
||||
},
|
||||
documentId: recipient.documentId,
|
||||
envelope: {
|
||||
id: recipient.envelopeId,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
},
|
||||
{
|
||||
recipientId: recipient.id,
|
||||
|
||||
@ -41,19 +41,19 @@ export const removeSignedFieldWithToken = async ({
|
||||
},
|
||||
},
|
||||
include: {
|
||||
document: true,
|
||||
envelope: true,
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { document } = field;
|
||||
const { envelope } = field;
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error(`Document not found for field ${field.id}`);
|
||||
}
|
||||
|
||||
if (document.status !== DocumentStatus.PENDING) {
|
||||
throw new Error(`Document ${document.id} must be pending`);
|
||||
if (envelope.status !== DocumentStatus.PENDING) {
|
||||
throw new Error(`Document ${envelope.id} must be pending`);
|
||||
}
|
||||
|
||||
if (
|
||||
@ -89,7 +89,7 @@ export const removeSignedFieldWithToken = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type { Field } from '@prisma/client';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { EnvelopeType, type Field, FieldType } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
|
||||
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
|
||||
@ -26,7 +25,7 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface SetFieldsForDocumentOptions {
|
||||
userId: number;
|
||||
@ -43,39 +42,49 @@ export const setFieldsForDocument = async ({
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: SetFieldsForDocumentOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
// Todo: Envelopes
|
||||
const firstEnvelopeItemId = envelope?.envelopeItems[0]?.id;
|
||||
|
||||
if (!envelope || !firstEnvelopeItemId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
}
|
||||
|
||||
const existingFields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId,
|
||||
},
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
const existingFields = envelope.fields;
|
||||
|
||||
const removedFields = existingFields.filter(
|
||||
(existingField) => !fields.find((field) => field.id === existingField.id),
|
||||
@ -84,7 +93,7 @@ export const setFieldsForDocument = async ({
|
||||
const linkedFields = fields.map((field) => {
|
||||
const existing = existingFields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
@ -197,7 +206,7 @@ export const setFieldsForDocument = async ({
|
||||
const upsertedField = await tx.field.upsert({
|
||||
where: {
|
||||
id: field._persisted?.id ?? -1,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
update: {
|
||||
page: field.pageNumber,
|
||||
@ -208,6 +217,8 @@ export const setFieldsForDocument = async ({
|
||||
fieldMeta: parsedFieldMeta,
|
||||
},
|
||||
create: {
|
||||
envelopeId: envelope.id,
|
||||
envelopeItemId: firstEnvelopeItemId, // Todo: Envelopes
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
@ -217,17 +228,7 @@ export const setFieldsForDocument = async ({
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
document: {
|
||||
connect: {
|
||||
id: documentId,
|
||||
},
|
||||
},
|
||||
recipient: {
|
||||
connect: {
|
||||
id: field.recipientId,
|
||||
documentId,
|
||||
},
|
||||
},
|
||||
recipientId: field._recipient.id,
|
||||
},
|
||||
});
|
||||
|
||||
@ -249,7 +250,7 @@ export const setFieldsForDocument = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
changes,
|
||||
@ -264,7 +265,7 @@ export const setFieldsForDocument = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
...baseAuditLog,
|
||||
@ -292,7 +293,7 @@ export const setFieldsForDocument = async ({
|
||||
data: removedFields.map((field) =>
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { EnvelopeType, FieldType } from '@prisma/client';
|
||||
|
||||
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
|
||||
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
|
||||
@ -16,7 +16,7 @@ import {
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type SetFieldsForTemplateOptions = {
|
||||
userId: number;
|
||||
@ -42,25 +42,40 @@ export const setFieldsForTemplate = async ({
|
||||
templateId,
|
||||
fields,
|
||||
}: SetFieldsForTemplateOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
// Todo: Envelopes
|
||||
const firstEnvelopeItemId = envelope?.envelopeItems[0]?.id;
|
||||
|
||||
if (!envelope || !firstEnvelopeItemId) {
|
||||
throw new Error('Template not found');
|
||||
}
|
||||
|
||||
const existingFields = await prisma.field.findMany({
|
||||
where: {
|
||||
templateId,
|
||||
},
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
const existingFields = envelope.fields;
|
||||
|
||||
const removedFields = existingFields.filter(
|
||||
(existingField) => !fields.find((field) => field.id === existingField.id),
|
||||
@ -143,7 +158,8 @@ export const setFieldsForTemplate = async ({
|
||||
return prisma.field.upsert({
|
||||
where: {
|
||||
id: field._persisted?.id ?? -1,
|
||||
templateId,
|
||||
envelopeId: envelope.id,
|
||||
envelopeItemId: firstEnvelopeItemId, // Todo: Envelopes
|
||||
},
|
||||
update: {
|
||||
page: field.pageNumber,
|
||||
@ -163,15 +179,20 @@ export const setFieldsForTemplate = async ({
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
template: {
|
||||
envelope: {
|
||||
connect: {
|
||||
id: templateId,
|
||||
id: envelope.id,
|
||||
},
|
||||
},
|
||||
envelopeItem: {
|
||||
connect: {
|
||||
id: firstEnvelopeItemId, // Todo: Envelopes
|
||||
},
|
||||
},
|
||||
recipient: {
|
||||
connect: {
|
||||
id: field.recipientId,
|
||||
templateId,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -81,7 +81,7 @@ export const signFieldWithToken = async ({
|
||||
},
|
||||
},
|
||||
include: {
|
||||
document: {
|
||||
envelope: {
|
||||
include: {
|
||||
recipients: true,
|
||||
},
|
||||
@ -90,9 +90,9 @@ export const signFieldWithToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const { document } = field;
|
||||
const { envelope } = field;
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error(`Document not found for field ${field.id}`);
|
||||
}
|
||||
|
||||
@ -100,12 +100,12 @@ export const signFieldWithToken = async ({
|
||||
throw new Error(`Recipient not found for field ${field.id}`);
|
||||
}
|
||||
|
||||
if (document.deletedAt) {
|
||||
throw new Error(`Document ${document.id} has been deleted`);
|
||||
if (envelope.deletedAt) {
|
||||
throw new Error(`Document ${envelope.id} has been deleted`);
|
||||
}
|
||||
|
||||
if (document.status !== DocumentStatus.PENDING) {
|
||||
throw new Error(`Document ${document.id} must be pending for signing`);
|
||||
if (envelope.status !== DocumentStatus.PENDING) {
|
||||
throw new Error(`Document ${envelope.id} must be pending for signing`);
|
||||
}
|
||||
|
||||
if (
|
||||
@ -172,7 +172,7 @@ export const signFieldWithToken = async ({
|
||||
}
|
||||
|
||||
const derivedRecipientActionAuth = await validateFieldAuth({
|
||||
documentAuthOptions: document.authOptions,
|
||||
documentAuthOptions: envelope.authOptions,
|
||||
recipient,
|
||||
field,
|
||||
userId,
|
||||
@ -181,7 +181,9 @@ export const signFieldWithToken = async ({
|
||||
|
||||
const documentMeta = await prisma.documentMeta.findFirst({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
envelope: {
|
||||
id: envelope.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -272,7 +274,7 @@ export const signFieldWithToken = async ({
|
||||
assistant && field.recipientId !== assistant.id
|
||||
? DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED
|
||||
: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
email: assistant?.email ?? recipient.email,
|
||||
name: assistant?.name ?? recipient.name,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { FieldType } from '@prisma/client';
|
||||
import { EnvelopeType, type FieldType } from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
@ -11,7 +11,7 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface UpdateDocumentFieldsOptions {
|
||||
userId: number;
|
||||
@ -37,34 +37,38 @@ export const updateDocumentFields = async ({
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentFieldsOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
}
|
||||
|
||||
const fieldsToUpdate = fields.map((field) => {
|
||||
const originalField = document.fields.find((existingField) => existingField.id === field.id);
|
||||
const originalField = envelope.fields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
if (!originalField) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
@ -72,7 +76,7 @@ export const updateDocumentFields = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const recipient = document.recipients.find(
|
||||
const recipient = envelope.recipients.find(
|
||||
(recipient) => recipient.id === originalField.recipientId,
|
||||
);
|
||||
|
||||
@ -84,7 +88,7 @@ export const updateDocumentFields = async ({
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can be modified.
|
||||
if (!canRecipientFieldsBeModified(recipient, document.fields)) {
|
||||
if (!canRecipientFieldsBeModified(recipient, envelope.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Cannot modify a field where the recipient has already interacted with the document',
|
||||
@ -123,7 +127,7 @@ export const updateDocumentFields = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: updatedField.secondaryId,
|
||||
|
||||
@ -1,119 +0,0 @@
|
||||
import type { FieldType, Team } from '@prisma/client';
|
||||
|
||||
import { type TFieldMetaSchema as FieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData, diffFieldChanges } from '../../utils/document-audit-logs';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type UpdateFieldOptions = {
|
||||
fieldId: number;
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
recipientId?: number;
|
||||
type?: FieldType;
|
||||
pageNumber?: number;
|
||||
pageX?: number;
|
||||
pageY?: number;
|
||||
pageWidth?: number;
|
||||
pageHeight?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
fieldMeta?: FieldMeta;
|
||||
};
|
||||
|
||||
export const updateField = async ({
|
||||
fieldId,
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
recipientId,
|
||||
type,
|
||||
pageNumber,
|
||||
pageX,
|
||||
pageY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
requestMetadata,
|
||||
fieldMeta,
|
||||
}: UpdateFieldOptions) => {
|
||||
if (type === 'FREE_SIGNATURE') {
|
||||
throw new Error('Cannot update a FREE_SIGNATURE field');
|
||||
}
|
||||
|
||||
const oldField = await prisma.field.findFirstOrThrow({
|
||||
where: {
|
||||
id: fieldId,
|
||||
document: {
|
||||
id: documentId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const field = prisma.$transaction(async (tx) => {
|
||||
const updatedField = await tx.field.update({
|
||||
where: {
|
||||
id: fieldId,
|
||||
},
|
||||
data: {
|
||||
recipientId,
|
||||
type,
|
||||
page: pageNumber,
|
||||
positionX: pageX,
|
||||
positionY: pageY,
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
fieldMeta,
|
||||
},
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({ teamId, userId }),
|
||||
});
|
||||
}
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
documentId,
|
||||
user: {
|
||||
id: team?.id ?? user.id,
|
||||
email: team?.name ?? user.email,
|
||||
name: team ? '' : user.name,
|
||||
},
|
||||
data: {
|
||||
fieldId: updatedField.secondaryId,
|
||||
fieldRecipientEmail: updatedField.recipient?.email ?? '',
|
||||
fieldRecipientId: recipientId ?? -1,
|
||||
fieldType: updatedField.type,
|
||||
changes: diffFieldChanges(oldField, updatedField),
|
||||
},
|
||||
requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return updatedField;
|
||||
});
|
||||
|
||||
return field;
|
||||
};
|
||||
@ -1,11 +1,11 @@
|
||||
import type { FieldType } from '@prisma/client';
|
||||
import { EnvelopeType, type FieldType } from '@prisma/client';
|
||||
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface UpdateTemplateFieldsOptions {
|
||||
userId: number;
|
||||
@ -29,25 +29,32 @@ export const updateTemplateFields = async ({
|
||||
templateId,
|
||||
fields,
|
||||
}: UpdateTemplateFieldsOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const fieldsToUpdate = fields.map((field) => {
|
||||
const originalField = template.fields.find((existingField) => existingField.id === field.id);
|
||||
const originalField = envelope.fields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
if (!originalField) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
@ -55,7 +62,7 @@ export const updateTemplateFields = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const recipient = template.recipients.find(
|
||||
const recipient = envelope.recipients.find(
|
||||
(recipient) => recipient.id === originalField.recipientId,
|
||||
);
|
||||
|
||||
@ -67,7 +74,7 @@ export const updateTemplateFields = async ({
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can be modified.
|
||||
if (!canRecipientFieldsBeModified(recipient, template.fields)) {
|
||||
if (!canRecipientFieldsBeModified(recipient, envelope.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Cannot modify a field where the recipient has already interacted with the document',
|
||||
|
||||
@ -2,8 +2,6 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { TFolderType } from '../../types/folder-type';
|
||||
import { FolderType } from '../../types/folder-type';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
|
||||
export interface CreateFolderOptions {
|
||||
@ -21,8 +19,7 @@ export const createFolder = async ({
|
||||
parentId,
|
||||
type = FolderType.DOCUMENT,
|
||||
}: CreateFolderOptions) => {
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
// This indirectly verifies whether the user has access to the team.
|
||||
const settings = await getTeamSettings({ userId, teamId });
|
||||
|
||||
return await prisma.folder.create({
|
||||
@ -32,7 +29,7 @@ export const createFolder = async ({
|
||||
teamId,
|
||||
parentId,
|
||||
type,
|
||||
visibility: determineDocumentVisibility(settings.documentVisibility, team.currentTeamRole),
|
||||
visibility: settings.documentVisibility,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export interface DeleteFolderOptions {
|
||||
@ -24,11 +21,6 @@ export const deleteFolder = async ({ userId, teamId, folderId }: DeleteFolderOpt
|
||||
userId,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
documents: true,
|
||||
subfolders: true,
|
||||
templates: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
@ -37,11 +29,7 @@ export const deleteFolder = async ({ userId, teamId, folderId }: DeleteFolderOpt
|
||||
});
|
||||
}
|
||||
|
||||
const hasPermission = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => folder.visibility !== DocumentVisibility.ADMIN)
|
||||
.with(TeamMemberRole.MEMBER, () => folder.visibility === DocumentVisibility.EVERYONE)
|
||||
.otherwise(() => false);
|
||||
const hasPermission = canAccessTeamDocument(team.currentTeamRole, folder.visibility);
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import type { TFolderType } from '../../types/folder-type';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
@ -17,22 +16,11 @@ export interface FindFoldersOptions {
|
||||
export const findFolders = async ({ userId, teamId, parentId, type }: FindFoldersOptions) => {
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
const visibilityFilters = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => ({
|
||||
visibility: {
|
||||
in: [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
DocumentVisibility.ADMIN,
|
||||
],
|
||||
},
|
||||
}))
|
||||
.with(TeamMemberRole.MANAGER, () => ({
|
||||
visibility: {
|
||||
in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE],
|
||||
},
|
||||
}))
|
||||
.otherwise(() => ({ visibility: DocumentVisibility.EVERYONE }));
|
||||
const visibilityFilters = {
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||
},
|
||||
};
|
||||
|
||||
const whereClause = {
|
||||
AND: [
|
||||
@ -69,13 +57,15 @@ export const findFolders = async ({ userId, teamId, parentId, type }: FindFolder
|
||||
createdAt: 'desc',
|
||||
},
|
||||
}),
|
||||
prisma.document.count({
|
||||
prisma.envelope.count({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
folderId: folder.id,
|
||||
},
|
||||
}),
|
||||
prisma.template.count({
|
||||
prisma.envelope.count({
|
||||
where: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
folderId: folder.id,
|
||||
},
|
||||
}),
|
||||
|
||||
@ -1,92 +0,0 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { FolderType } from '@documenso/lib/types/folder-type';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export interface MoveDocumentToFolderOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
folderId?: string | null;
|
||||
requestMetadata?: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const moveDocumentToFolder = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
folderId,
|
||||
}: MoveDocumentToFolderOptions) => {
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
const visibilityFilters = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => ({
|
||||
visibility: {
|
||||
in: [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
DocumentVisibility.ADMIN,
|
||||
],
|
||||
},
|
||||
}))
|
||||
.with(TeamMemberRole.MANAGER, () => ({
|
||||
visibility: {
|
||||
in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE],
|
||||
},
|
||||
}))
|
||||
.otherwise(() => ({ visibility: DocumentVisibility.EVERYONE }));
|
||||
|
||||
const documentWhereClause = {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
};
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereClause,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (folderId) {
|
||||
const folderWhereClause = {
|
||||
id: folderId,
|
||||
type: FolderType.DOCUMENT,
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
};
|
||||
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: folderWhereClause,
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.document.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
data: {
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,63 +0,0 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { FolderType } from '@documenso/lib/types/folder-type';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export interface MoveTemplateToFolderOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
templateId: number;
|
||||
folderId?: string | null;
|
||||
}
|
||||
|
||||
export const moveTemplateToFolder = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
folderId,
|
||||
}: MoveTemplateToFolderOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (folderId !== null) {
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: folderId,
|
||||
team: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
}),
|
||||
type: FolderType.TEMPLATE,
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.template.update({
|
||||
where: {
|
||||
id: templateId,
|
||||
},
|
||||
data: {
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Template, TemplateDirectLink } from '@prisma/client';
|
||||
import { type TeamProfile, TemplateType } from '@prisma/client';
|
||||
import type { Envelope, TemplateDirectLink } from '@prisma/client';
|
||||
import { EnvelopeType, type TeamProfile, TemplateType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -9,11 +9,8 @@ export type GetPublicProfileByUrlOptions = {
|
||||
profileUrl: string;
|
||||
};
|
||||
|
||||
type PublicDirectLinkTemplate = Template & {
|
||||
type: 'PUBLIC';
|
||||
directLink: TemplateDirectLink & {
|
||||
enabled: true;
|
||||
};
|
||||
type PublicDirectLinkTemplate = Pick<Envelope, 'id' | 'publicTitle' | 'publicDescription'> & {
|
||||
directLink: TemplateDirectLink;
|
||||
};
|
||||
|
||||
type GetPublicProfileByUrlResponse = {
|
||||
@ -43,12 +40,13 @@ export const getPublicProfileByUrl = async ({
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
templates: {
|
||||
envelopes: {
|
||||
where: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: TemplateType.PUBLIC,
|
||||
directLink: {
|
||||
enabled: true,
|
||||
},
|
||||
type: TemplateType.PUBLIC,
|
||||
},
|
||||
include: {
|
||||
directLink: true,
|
||||
@ -68,13 +66,28 @@ export const getPublicProfileByUrl = async ({
|
||||
type: 'Premium',
|
||||
since: team.createdAt,
|
||||
},
|
||||
profile: team.profile,
|
||||
profile: {
|
||||
teamId: team.profile.teamId,
|
||||
id: team.profile.id,
|
||||
enabled: team.profile.enabled,
|
||||
bio: team.profile.bio,
|
||||
},
|
||||
url: profileUrl,
|
||||
avatarImageId: team.avatarImageId,
|
||||
name: team.name || '',
|
||||
templates: team.templates.filter(
|
||||
(template): template is PublicDirectLinkTemplate =>
|
||||
template.directLink?.enabled === true && template.type === TemplateType.PUBLIC,
|
||||
),
|
||||
templates: team.envelopes.map((template) => {
|
||||
const directLink = template.directLink;
|
||||
|
||||
if (!directLink || !directLink.enabled || template.templateType !== TemplateType.PUBLIC) {
|
||||
throw new Error('Not possible');
|
||||
}
|
||||
|
||||
return {
|
||||
id: template.id,
|
||||
publicTitle: template.publicTitle,
|
||||
publicDescription: template.publicDescription,
|
||||
directLink,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
@ -11,12 +11,13 @@ import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface CreateDocumentRecipientsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
recipients: {
|
||||
email: string;
|
||||
name: string;
|
||||
@ -31,18 +32,19 @@ export interface CreateDocumentRecipientsOptions {
|
||||
export const createDocumentRecipients = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
id,
|
||||
recipients: recipientsToCreate,
|
||||
requestMetadata,
|
||||
}: CreateDocumentRecipientsOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
team: {
|
||||
@ -57,13 +59,13 @@ export const createDocumentRecipients = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
@ -74,7 +76,7 @@ export const createDocumentRecipients = async ({
|
||||
);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth && !document.team.organisation.organisationClaim.flags.cfr21) {
|
||||
if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
@ -95,7 +97,7 @@ export const createDocumentRecipients = async ({
|
||||
|
||||
const createdRecipient = await tx.recipient.create({
|
||||
data: {
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
@ -112,7 +114,7 @@ export const createDocumentRecipients = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: createdRecipient.email,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
@ -8,7 +8,7 @@ import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface CreateTemplateRecipientsOptions {
|
||||
userId: number;
|
||||
@ -30,11 +30,18 @@ export const createTemplateRecipients = async ({
|
||||
templateId,
|
||||
recipients: recipientsToCreate,
|
||||
}: CreateTemplateRecipientsOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const template = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
team: {
|
||||
@ -81,7 +88,7 @@ export const createTemplateRecipients = async ({
|
||||
|
||||
const createdRecipient = await tx.recipient.create({
|
||||
data: {
|
||||
templateId,
|
||||
envelopeId: template.id,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { SendStatus } from '@prisma/client';
|
||||
import { EnvelopeType, SendStatus } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
||||
@ -30,9 +30,10 @@ export const deleteDocumentRecipient = async ({
|
||||
teamId,
|
||||
recipientId,
|
||||
requestMetadata,
|
||||
}: DeleteDocumentRecipientOptions): Promise<void> => {
|
||||
const document = await prisma.document.findFirst({
|
||||
}: DeleteDocumentRecipientOptions) => {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
@ -62,13 +63,13 @@ export const deleteDocumentRecipient = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
@ -80,7 +81,7 @@ export const deleteDocumentRecipient = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const recipientToDelete = document.recipients[0];
|
||||
const recipientToDelete = envelope.recipients[0];
|
||||
|
||||
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
@ -88,17 +89,11 @@ export const deleteDocumentRecipient = async ({
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.recipient.delete({
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
});
|
||||
|
||||
const deletedRecipient = await prisma.$transaction(async (tx) => {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: recipientToDelete.email,
|
||||
@ -108,10 +103,16 @@ export const deleteDocumentRecipient = async ({
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return await tx.recipient.delete({
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).recipientRemoved;
|
||||
|
||||
// Send email to deleted recipient.
|
||||
@ -119,8 +120,8 @@ export const deleteDocumentRecipient = async ({
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(RecipientRemovedFromDocumentTemplate, {
|
||||
documentName: document.title,
|
||||
inviterName: document.team?.name || user.name || undefined,
|
||||
documentName: envelope.title,
|
||||
inviterName: envelope.team?.name || user.name || undefined,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
@ -128,9 +129,9 @@ export const deleteDocumentRecipient = async ({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
@ -152,4 +153,6 @@ export const deleteDocumentRecipient = async ({
|
||||
text,
|
||||
});
|
||||
}
|
||||
|
||||
return deletedRecipient;
|
||||
};
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
import { SendStatus } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type DeleteRecipientOptions = {
|
||||
documentId: number;
|
||||
recipientId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const deleteRecipient = async ({
|
||||
documentId,
|
||||
recipientId,
|
||||
userId,
|
||||
teamId,
|
||||
requestMetadata,
|
||||
}: DeleteRecipientOptions) => {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
id: recipientId,
|
||||
document: {
|
||||
id: documentId,
|
||||
userId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
if (recipient.sendStatus !== SendStatus.NOT_SENT) {
|
||||
throw new Error('Can not delete a recipient that has already been sent a document');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({ teamId, userId }),
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const deletedRecipient = await prisma.$transaction(async (tx) => {
|
||||
const deleted = await tx.recipient.delete({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: 'RECIPIENT_DELETED',
|
||||
documentId,
|
||||
user: {
|
||||
id: team?.id ?? user.id,
|
||||
email: team?.name ?? user.email,
|
||||
name: team ? '' : user.name,
|
||||
},
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
},
|
||||
requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return deleted;
|
||||
});
|
||||
|
||||
return deletedRecipient;
|
||||
};
|
||||
@ -1,7 +1,10 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface DeleteTemplateRecipientOptions {
|
||||
userId: number;
|
||||
@ -14,31 +17,31 @@ export const deleteTemplateRecipient = async ({
|
||||
teamId,
|
||||
recipientId,
|
||||
}: DeleteTemplateRecipientOptions): Promise<void> => {
|
||||
const template = await prisma.template.findFirst({
|
||||
const recipientToDelete = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
id: recipientId,
|
||||
envelope: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
if (!recipientToDelete) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
message: 'Recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
const recipientToDelete = template.recipients[0];
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: recipientToDelete.envelopeId,
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
@ -49,6 +52,7 @@ export const deleteTemplateRecipient = async ({
|
||||
await prisma.recipient.delete({
|
||||
where: {
|
||||
id: recipientId,
|
||||
envelope: envelopeWhereInput,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DocumentSigningOrder, SigningStatus } from '@prisma/client';
|
||||
import { DocumentSigningOrder, EnvelopeType, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -7,8 +7,9 @@ export type GetIsRecipientTurnOptions = {
|
||||
};
|
||||
|
||||
export async function getIsRecipientsTurnToSign({ token }: GetIsRecipientTurnOptions) {
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
@ -25,11 +26,11 @@ export async function getIsRecipientsTurnToSign({ token }: GetIsRecipientTurnOpt
|
||||
},
|
||||
});
|
||||
|
||||
if (document.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL) {
|
||||
if (envelope.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { recipients } = document;
|
||||
const { recipients } = envelope;
|
||||
|
||||
const currentRecipientIndex = recipients.findIndex((r) => r.token === token);
|
||||
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { mapDocumentIdToSecondaryId } from '../../utils/envelope';
|
||||
|
||||
export const getNextPendingRecipient = async ({
|
||||
documentId,
|
||||
currentRecipientId,
|
||||
@ -9,7 +13,10 @@ export const getNextPendingRecipient = async ({
|
||||
}) => {
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId,
|
||||
envelope: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
secondaryId: mapDocumentIdToSecondaryId(documentId),
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetRecipientByEmailOptions = {
|
||||
documentId: number;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export const getRecipientByEmail = async ({ documentId, email }: GetRecipientByEmailOptions) => {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
documentId,
|
||||
email: email.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
return recipient;
|
||||
};
|
||||
@ -1,21 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetRecipientByIdOptions = {
|
||||
id: number;
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const getRecipientByIdV1Api = async ({ documentId, id }: GetRecipientByIdOptions) => {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
documentId,
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
return recipient;
|
||||
};
|
||||
@ -1,12 +1,17 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { mapSecondaryIdToDocumentId, mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type GetRecipientByIdOptions = {
|
||||
recipientId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
type: EnvelopeType;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -17,16 +22,23 @@ export const getRecipientById = async ({
|
||||
recipientId,
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
}: GetRecipientByIdOptions) => {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
id: recipientId,
|
||||
document: {
|
||||
envelope: {
|
||||
type,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
fields: true,
|
||||
envelope: {
|
||||
select: {
|
||||
secondaryId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -36,5 +48,24 @@ export const getRecipientById = async ({
|
||||
});
|
||||
}
|
||||
|
||||
return recipient;
|
||||
const legacyId = match(type)
|
||||
.with(EnvelopeType.DOCUMENT, () => ({
|
||||
documentId: mapSecondaryIdToDocumentId(recipient.envelope.secondaryId),
|
||||
}))
|
||||
.with(EnvelopeType.TEMPLATE, () => ({
|
||||
templateId: mapSecondaryIdToTemplateId(recipient.envelope.secondaryId),
|
||||
}))
|
||||
.exhaustive();
|
||||
|
||||
// Backwards compatibility mapping.
|
||||
return {
|
||||
...recipient,
|
||||
...legacyId,
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
fields: recipient.fields.map((field) => ({
|
||||
...field,
|
||||
...legacyId,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { EnvelopeType, Prisma } from '@prisma/client';
|
||||
|
||||
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetRecipientSuggestionsOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
query: string;
|
||||
};
|
||||
|
||||
@ -37,7 +37,8 @@ export const getRecipientSuggestions = async ({
|
||||
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
document: {
|
||||
envelope: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
...nameEmailFilter,
|
||||
@ -45,7 +46,7 @@ export const getRecipientSuggestions = async ({
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
document: {
|
||||
envelope: {
|
||||
select: {
|
||||
createdAt: true,
|
||||
},
|
||||
@ -53,7 +54,7 @@ export const getRecipientSuggestions = async ({
|
||||
},
|
||||
distinct: ['email'],
|
||||
orderBy: {
|
||||
document: {
|
||||
envelope: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
},
|
||||
|
||||
@ -23,7 +23,7 @@ export const getRecipientsForAssistant = async ({ token }: GetRecipientsForAssis
|
||||
|
||||
let recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId: assistant.documentId,
|
||||
envelopeId: assistant.envelopeId,
|
||||
signingOrder: {
|
||||
gte: assistant.signingOrder ?? 0,
|
||||
},
|
||||
@ -39,7 +39,7 @@ export const getRecipientsForAssistant = async ({ token }: GetRecipientsForAssis
|
||||
type: {
|
||||
not: FieldType.SIGNATURE,
|
||||
},
|
||||
documentId: assistant.documentId,
|
||||
envelopeId: assistant.envelopeId,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface GetRecipientsForDocumentOptions {
|
||||
documentId: number;
|
||||
@ -13,15 +15,19 @@ export const getRecipientsForDocument = async ({
|
||||
userId,
|
||||
teamId,
|
||||
}: GetRecipientsForDocumentOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
document: documentWhereInput,
|
||||
envelope: envelopeWhereInput,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
|
||||
@ -2,7 +2,7 @@ import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
|
||||
@ -29,8 +29,8 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { canRecipientBeModified } from '../../utils/recipients';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface SetDocumentRecipientsOptions {
|
||||
userId: number;
|
||||
@ -47,14 +47,18 @@ export const setDocumentRecipients = async ({
|
||||
recipients,
|
||||
requestMetadata,
|
||||
}: SetDocumentRecipientsOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
fields: true,
|
||||
documentMeta: true,
|
||||
@ -67,6 +71,7 @@ export const setDocumentRecipients = async ({
|
||||
},
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -81,11 +86,11 @@ export const setDocumentRecipients = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
if (envelope.completedAt) {
|
||||
throw new Error('Document already complete');
|
||||
}
|
||||
|
||||
@ -95,7 +100,7 @@ export const setDocumentRecipients = async ({
|
||||
type: 'team',
|
||||
teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const recipientsHaveActionAuth = recipients.some(
|
||||
@ -103,7 +108,7 @@ export const setDocumentRecipients = async ({
|
||||
);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth && !document.team.organisation.organisationClaim.flags.cfr21) {
|
||||
if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
@ -114,11 +119,7 @@ export const setDocumentRecipients = async ({
|
||||
email: recipient.email.toLowerCase(),
|
||||
}));
|
||||
|
||||
const existingRecipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
const existingRecipients = envelope.recipients;
|
||||
|
||||
const removedRecipients = existingRecipients.filter(
|
||||
(existingRecipient) =>
|
||||
@ -131,12 +132,12 @@ export const setDocumentRecipients = async ({
|
||||
);
|
||||
|
||||
const canPersistedRecipientBeModified =
|
||||
existing && canRecipientBeModified(existing, document.fields);
|
||||
existing && canRecipientBeModified(existing, envelope.fields);
|
||||
|
||||
if (
|
||||
existing &&
|
||||
hasRecipientBeenChanged(existing, recipient) &&
|
||||
!canRecipientBeModified(existing, document.fields)
|
||||
!canRecipientBeModified(existing, envelope.fields)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Cannot modify a recipient who has already interacted with the document',
|
||||
@ -172,14 +173,14 @@ export const setDocumentRecipients = async ({
|
||||
const upsertedRecipient = await tx.recipient.upsert({
|
||||
where: {
|
||||
id: recipient._persisted?.id ?? -1,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
update: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
@ -191,7 +192,7 @@ export const setDocumentRecipients = async ({
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
@ -230,7 +231,7 @@ export const setDocumentRecipients = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
changes,
|
||||
@ -245,7 +246,7 @@ export const setDocumentRecipients = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
...baseAuditLog,
|
||||
@ -278,7 +279,7 @@ export const setDocumentRecipients = async ({
|
||||
data: removedRecipients.map((recipient) =>
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
@ -292,7 +293,7 @@ export const setDocumentRecipients = async ({
|
||||
});
|
||||
|
||||
const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).recipientRemoved;
|
||||
|
||||
// Send emails to deleted recipients.
|
||||
@ -305,7 +306,7 @@ export const setDocumentRecipients = async ({
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(RecipientRemovedFromDocumentTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
inviterName: user.name || undefined,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
|
||||
import {
|
||||
DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||
@ -14,12 +14,13 @@ import {
|
||||
} from '../../types/document-auth';
|
||||
import { nanoid } from '../../universal/id';
|
||||
import { createRecipientAuthOptions } from '../../utils/document-auth';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type SetTemplateRecipientsOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
templateId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
recipients: {
|
||||
id?: number;
|
||||
email: string;
|
||||
@ -33,14 +34,18 @@ export type SetTemplateRecipientsOptions = {
|
||||
export const setTemplateRecipients = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
id,
|
||||
recipients,
|
||||
}: SetTemplateRecipientsOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
directLink: true,
|
||||
team: {
|
||||
@ -52,10 +57,11 @@ export const setTemplateRecipients = async ({
|
||||
},
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
if (!envelope) {
|
||||
throw new Error('Template not found');
|
||||
}
|
||||
|
||||
@ -64,7 +70,7 @@ export const setTemplateRecipients = async ({
|
||||
);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth && !template.team.organisation.organisationClaim.flags.cfr21) {
|
||||
if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
@ -72,7 +78,7 @@ export const setTemplateRecipients = async ({
|
||||
|
||||
const normalizedRecipients = recipients.map((recipient) => {
|
||||
// Force replace any changes to the name or email of the direct recipient.
|
||||
if (template.directLink && recipient.id === template.directLink.directTemplateRecipientId) {
|
||||
if (envelope.directLink && recipient.id === envelope.directLink.directTemplateRecipientId) {
|
||||
return {
|
||||
...recipient,
|
||||
email: DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||
@ -86,24 +92,20 @@ export const setTemplateRecipients = async ({
|
||||
};
|
||||
});
|
||||
|
||||
const existingRecipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
templateId,
|
||||
},
|
||||
});
|
||||
const existingRecipients = envelope.recipients;
|
||||
|
||||
const removedRecipients = existingRecipients.filter(
|
||||
(existingRecipient) =>
|
||||
!normalizedRecipients.find((recipient) => recipient.id === existingRecipient.id),
|
||||
);
|
||||
|
||||
if (template.directLink !== null) {
|
||||
if (envelope.directLink !== null) {
|
||||
const updatedDirectRecipient = recipients.find(
|
||||
(recipient) => recipient.id === template.directLink?.directTemplateRecipientId,
|
||||
(recipient) => recipient.id === envelope.directLink?.directTemplateRecipientId,
|
||||
);
|
||||
|
||||
const deletedDirectRecipient = removedRecipients.find(
|
||||
(recipient) => recipient.id === template.directLink?.directTemplateRecipientId,
|
||||
(recipient) => recipient.id === envelope.directLink?.directTemplateRecipientId,
|
||||
);
|
||||
|
||||
if (updatedDirectRecipient?.role === RecipientRole.CC) {
|
||||
@ -145,14 +147,14 @@ export const setTemplateRecipients = async ({
|
||||
const upsertedRecipient = await tx.recipient.upsert({
|
||||
where: {
|
||||
id: recipient._persisted?.id ?? -1,
|
||||
templateId,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
update: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
templateId,
|
||||
envelopeId: envelope.id,
|
||||
authOptions,
|
||||
},
|
||||
create: {
|
||||
@ -161,7 +163,7 @@ export const setTemplateRecipients = async ({
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
templateId,
|
||||
envelopeId: envelope.id,
|
||||
authOptions,
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
@ -16,13 +16,14 @@ import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { canRecipientBeModified } from '../../utils/recipients';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface UpdateDocumentRecipientsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
recipients: RecipientData[];
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
@ -30,18 +31,19 @@ export interface UpdateDocumentRecipientsOptions {
|
||||
export const updateDocumentRecipients = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
id,
|
||||
recipients,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentRecipientsOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
fields: true,
|
||||
recipients: true,
|
||||
@ -57,13 +59,13 @@ export const updateDocumentRecipients = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
@ -74,14 +76,14 @@ export const updateDocumentRecipients = async ({
|
||||
);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth && !document.team.organisation.organisationClaim.flags.cfr21) {
|
||||
if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
|
||||
const recipientsToUpdate = recipients.map((recipient) => {
|
||||
const originalRecipient = document.recipients.find(
|
||||
const originalRecipient = envelope.recipients.find(
|
||||
(existingRecipient) => existingRecipient.id === recipient.id,
|
||||
);
|
||||
|
||||
@ -91,7 +93,7 @@ export const updateDocumentRecipients = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (!canRecipientBeModified(originalRecipient, document.fields)) {
|
||||
if (!canRecipientBeModified(originalRecipient, envelope.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Cannot modify a recipient who has already interacted with the document',
|
||||
});
|
||||
@ -123,14 +125,14 @@ export const updateDocumentRecipients = async ({
|
||||
const updatedRecipient = await tx.recipient.update({
|
||||
where: {
|
||||
id: originalRecipient.id,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
data: {
|
||||
name: mergedRecipient.name,
|
||||
email: mergedRecipient.email,
|
||||
role: mergedRecipient.role,
|
||||
signingOrder: mergedRecipient.signingOrder,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
sendStatus:
|
||||
mergedRecipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
@ -164,7 +166,7 @@ export const updateDocumentRecipients = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: updatedRecipient.email,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
@ -10,7 +10,7 @@ import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface UpdateTemplateRecipientsOptions {
|
||||
userId: number;
|
||||
@ -33,11 +33,18 @@ export const updateTemplateRecipients = async ({
|
||||
templateId,
|
||||
recipients,
|
||||
}: UpdateTemplateRecipientsOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
team: {
|
||||
@ -52,7 +59,7 @@ export const updateTemplateRecipients = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
@ -63,14 +70,14 @@ export const updateTemplateRecipients = async ({
|
||||
);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth && !template.team.organisation.organisationClaim.flags.cfr21) {
|
||||
if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
|
||||
const recipientsToUpdate = recipients.map((recipient) => {
|
||||
const originalRecipient = template.recipients.find(
|
||||
const originalRecipient = envelope.recipients.find(
|
||||
(existingRecipient) => existingRecipient.id === recipient.id,
|
||||
);
|
||||
|
||||
@ -109,14 +116,14 @@ export const updateTemplateRecipients = async ({
|
||||
const updatedRecipient = await tx.recipient.update({
|
||||
where: {
|
||||
id: originalRecipient.id,
|
||||
templateId,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
data: {
|
||||
name: mergedRecipient.name,
|
||||
email: mergedRecipient.email,
|
||||
role: mergedRecipient.role,
|
||||
signingOrder: mergedRecipient.signingOrder,
|
||||
templateId,
|
||||
envelopeId: envelope.id,
|
||||
sendStatus:
|
||||
mergedRecipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { alphaid } from '../../universal/id';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
|
||||
export type CreateSharingIdOptions =
|
||||
| {
|
||||
@ -15,12 +18,29 @@ export type CreateSharingIdOptions =
|
||||
};
|
||||
|
||||
export const createOrGetShareLink = async ({ documentId, ...options }: CreateSharingIdOptions) => {
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: unsafeBuildEnvelopeIdQuery(
|
||||
{
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
EnvelopeType.DOCUMENT,
|
||||
),
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const email = await match(options)
|
||||
.with({ token: P.string }, async ({ token }) => {
|
||||
return await prisma.recipient
|
||||
.findFirst({
|
||||
where: {
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
token,
|
||||
},
|
||||
})
|
||||
@ -32,6 +52,9 @@ export const createOrGetShareLink = async ({ documentId, ...options }: CreateSha
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
})
|
||||
.then((user) => user?.email);
|
||||
})
|
||||
@ -43,14 +66,14 @@ export const createOrGetShareLink = async ({ documentId, ...options }: CreateSha
|
||||
|
||||
return await prisma.documentShareLink.upsert({
|
||||
where: {
|
||||
documentId_email: {
|
||||
envelopeId_email: {
|
||||
envelopeId: envelope.id,
|
||||
email,
|
||||
documentId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
email,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
slug: alphaid(14),
|
||||
},
|
||||
update: {},
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetShareLinkBySlugOptions = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export const getShareLinkBySlug = async ({ slug }: GetShareLinkBySlugOptions) => {
|
||||
return await prisma.documentShareLink.findFirstOrThrow({
|
||||
where: {
|
||||
slug,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -21,7 +21,14 @@ type GetMemberRolesOptions = {
|
||||
* Returns the highest Team role of a given member or user of a team
|
||||
*/
|
||||
export const getMemberRoles = async ({ teamId, reference }: GetMemberRolesOptions) => {
|
||||
const team = await prisma.team.findFirst({
|
||||
// Enforce incase teamId undefined slips through due to invalid types.
|
||||
if (teamId === undefined) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
const team = await prisma.team.findUnique({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
|
||||
@ -5,6 +5,7 @@ import type { Field, Signature } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
FieldType,
|
||||
Prisma,
|
||||
RecipientRole,
|
||||
@ -31,7 +32,7 @@ import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/doc
|
||||
import { ZFieldMetaSchema } from '../../types/field-meta';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { isRequiredField } from '../../utils/advanced-fields-helpers';
|
||||
@ -43,11 +44,13 @@ import {
|
||||
createRecipientAuthOptions,
|
||||
extractDocumentAuthMethods,
|
||||
} from '../../utils/document-auth';
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { formatDocumentsPath } from '../../utils/teams';
|
||||
import { sendDocument } from '../document/send-document';
|
||||
import { validateFieldAuth } from '../document/validate-field-auth';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { incrementDocumentId } from '../envelope/increment-id';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentFromDirectTemplateOptions = {
|
||||
@ -90,7 +93,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
requestMetadata,
|
||||
user,
|
||||
}: CreateDocumentFromDirectTemplateOptions): Promise<TCreateDocumentFromDirectTemplateResponse> => {
|
||||
const template = await prisma.template.findFirst({
|
||||
const directTemplateEnvelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
directLink: {
|
||||
token: directTemplateToken,
|
||||
@ -103,8 +106,12 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
},
|
||||
},
|
||||
directLink: true,
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
envelopeItems: {
|
||||
select: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
@ -115,19 +122,31 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!template?.directLink?.enabled) {
|
||||
if (!directTemplateEnvelope?.directLink?.enabled) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Invalid or missing template' });
|
||||
}
|
||||
|
||||
const directTemplateEnvelopeLegacyId = mapSecondaryIdToTemplateId(
|
||||
directTemplateEnvelope.secondaryId,
|
||||
);
|
||||
const firstEnvelopeItem = directTemplateEnvelope.envelopeItems[0];
|
||||
|
||||
// Todo: Envelopes
|
||||
if (directTemplateEnvelope.envelopeItems.length !== 1 || !firstEnvelopeItem) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Invalid number of envelope items',
|
||||
});
|
||||
}
|
||||
|
||||
const { branding, settings, senderEmail, emailLanguage } = await getEmailContext({
|
||||
emailType: 'INTERNAL',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: template.teamId,
|
||||
teamId: directTemplateEnvelope.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
const { recipients, directLink, user: templateOwner } = template;
|
||||
const { recipients, directLink, user: templateOwner } = directTemplateEnvelope;
|
||||
|
||||
const directTemplateRecipient = recipients.find(
|
||||
(recipient) => recipient.id === directLink.directTemplateRecipientId,
|
||||
@ -139,7 +158,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (template.updatedAt.getTime() !== templateUpdatedAt.getTime()) {
|
||||
if (directTemplateEnvelope.updatedAt.getTime() !== templateUpdatedAt.getTime()) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Template no longer matches' });
|
||||
}
|
||||
|
||||
@ -151,7 +170,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
const { derivedRecipientAccessAuth, documentAuthOption: templateAuthOptions } =
|
||||
extractDocumentAuthMethods({
|
||||
documentAuth: template.authOptions,
|
||||
documentAuth: directTemplateEnvelope.authOptions,
|
||||
});
|
||||
|
||||
const directRecipientName = user?.name || initialDirectRecipientName;
|
||||
@ -171,10 +190,13 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
directTemplateRecipient.authOptions,
|
||||
);
|
||||
|
||||
const nonDirectTemplateRecipients = template.recipients.filter(
|
||||
const nonDirectTemplateRecipients = directTemplateEnvelope.recipients.filter(
|
||||
(recipient) => recipient.id !== directTemplateRecipient.id,
|
||||
);
|
||||
const derivedDocumentMeta = extractDerivedDocumentMeta(settings, template.templateMeta);
|
||||
const derivedDocumentMeta = extractDerivedDocumentMeta(
|
||||
settings,
|
||||
directTemplateEnvelope.documentMeta,
|
||||
);
|
||||
|
||||
// Associate, validate and map to a query every direct template recipient field with the provided fields.
|
||||
// Only process fields that are either required or have been signed by the user
|
||||
@ -202,7 +224,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
}
|
||||
|
||||
const derivedRecipientActionAuth = await validateFieldAuth({
|
||||
documentAuthOptions: template.authOptions,
|
||||
documentAuthOptions: directTemplateEnvelope.authOptions,
|
||||
recipient: {
|
||||
authOptions: directTemplateRecipient.authOptions,
|
||||
email: directRecipientEmail,
|
||||
@ -267,29 +289,45 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
const initialRequestTime = new Date();
|
||||
|
||||
const { documentId, recipientId, token } = await prisma.$transaction(async (tx) => {
|
||||
const documentData = await tx.documentData.create({
|
||||
data: {
|
||||
type: template.templateDocumentData.type,
|
||||
data: template.templateDocumentData.data,
|
||||
initialData: template.templateDocumentData.initialData,
|
||||
},
|
||||
});
|
||||
// Todo: Envelopes
|
||||
const documentData = await prisma.documentData.create({
|
||||
data: {
|
||||
type: firstEnvelopeItem.documentData.type,
|
||||
data: firstEnvelopeItem.documentData.data,
|
||||
initialData: firstEnvelopeItem.documentData.initialData,
|
||||
},
|
||||
});
|
||||
|
||||
// Create the document and non direct template recipients.
|
||||
const document = await tx.document.create({
|
||||
const documentMeta = await prisma.documentMeta.create({
|
||||
data: derivedDocumentMeta,
|
||||
});
|
||||
|
||||
const incrementedDocumentId = await incrementDocumentId();
|
||||
|
||||
const { createdEnvelope, recipientId, token } = await prisma.$transaction(async (tx) => {
|
||||
// Create the envelope and non direct template recipients.
|
||||
const createdEnvelope = await tx.envelope.create({
|
||||
data: {
|
||||
id: prefixedId('envelope'),
|
||||
secondaryId: incrementedDocumentId.formattedDocumentId,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
qrToken: prefixedId('qr'),
|
||||
source: DocumentSource.TEMPLATE_DIRECT_LINK,
|
||||
templateId: template.id,
|
||||
userId: template.userId,
|
||||
teamId: template.teamId,
|
||||
title: template.title,
|
||||
templateId: directTemplateEnvelopeLegacyId,
|
||||
userId: directTemplateEnvelope.userId,
|
||||
teamId: directTemplateEnvelope.teamId,
|
||||
title: directTemplateEnvelope.title,
|
||||
createdAt: initialRequestTime,
|
||||
status: DocumentStatus.PENDING,
|
||||
externalId: directTemplateExternalId,
|
||||
visibility: settings.documentVisibility,
|
||||
documentDataId: documentData.id,
|
||||
envelopeItems: {
|
||||
create: {
|
||||
id: prefixedId('envelope_item'),
|
||||
title: directTemplateEnvelope.title, // Todo: Envelopes use item title instead
|
||||
documentDataId: documentData.id,
|
||||
},
|
||||
},
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: templateAuthOptions.globalAccessAuth,
|
||||
globalActionAuth: templateAuthOptions.globalActionAuth,
|
||||
@ -319,9 +357,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
}),
|
||||
},
|
||||
},
|
||||
documentMeta: {
|
||||
create: derivedDocumentMeta,
|
||||
},
|
||||
documentMetaId: documentMeta.id,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
@ -330,13 +366,20 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const envelopeItemId = createdEnvelope.envelopeItems[0].id;
|
||||
|
||||
let nonDirectRecipientFieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
|
||||
|
||||
Object.values(nonDirectTemplateRecipients).forEach((templateRecipient) => {
|
||||
const recipient = document.recipients.find(
|
||||
const recipient = createdEnvelope.recipients.find(
|
||||
(recipient) => recipient.email === templateRecipient.email,
|
||||
);
|
||||
|
||||
@ -346,7 +389,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
nonDirectRecipientFieldsToCreate = nonDirectRecipientFieldsToCreate.concat(
|
||||
templateRecipient.fields.map((field) => ({
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
envelopeItemId: envelopeItemId, // Todo: Envelopes
|
||||
recipientId: recipient.id,
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
@ -371,7 +415,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
// Create the direct recipient and their non signature fields.
|
||||
const createdDirectRecipient = await tx.recipient.create({
|
||||
data: {
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
email: directRecipientEmail,
|
||||
name: directRecipientName,
|
||||
authOptions: createRecipientAuthOptions({
|
||||
@ -387,7 +431,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
fields: {
|
||||
createMany: {
|
||||
data: directTemplateNonSignatureFields.map(({ templateField, customText }) => ({
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
envelopeItemId: envelopeItemId, // Todo: Envelopes
|
||||
type: templateField.type,
|
||||
page: templateField.page,
|
||||
positionX: templateField.positionX,
|
||||
@ -417,7 +462,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
const field = await tx.field.create({
|
||||
data: {
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
envelopeItemId: envelopeItemId, // Todo: Envelopes
|
||||
recipientId: createdDirectRecipient.id,
|
||||
type: templateField.type,
|
||||
page: templateField.page,
|
||||
@ -466,7 +512,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
const auditLogsToCreate: CreateDocumentAuditLogDataResponse[] = [
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
user: {
|
||||
id: user?.id,
|
||||
name: user?.name,
|
||||
@ -474,17 +520,17 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
},
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
title: document.title,
|
||||
title: createdEnvelope.title,
|
||||
source: {
|
||||
type: DocumentSource.TEMPLATE_DIRECT_LINK,
|
||||
templateId: template.id,
|
||||
templateId: directTemplateEnvelopeLegacyId,
|
||||
directRecipientEmail,
|
||||
},
|
||||
},
|
||||
}),
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
user: {
|
||||
id: user?.id,
|
||||
name: user?.name,
|
||||
@ -502,7 +548,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
...createdDirectRecipientFields.map(({ field, derivedRecipientActionAuth }) =>
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
user: {
|
||||
id: user?.id,
|
||||
name: user?.name,
|
||||
@ -547,7 +593,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
),
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
user: {
|
||||
id: user?.id,
|
||||
name: user?.name,
|
||||
@ -572,10 +618,10 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
const emailTemplate = createElement(DocumentCreatedFromDirectTemplateEmailTemplate, {
|
||||
recipientName: directRecipientEmail,
|
||||
recipientRole: directTemplateRecipient.role,
|
||||
documentLink: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(document.team?.url)}/${
|
||||
document.id
|
||||
documentLink: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(createdEnvelope.team?.url)}/${
|
||||
createdEnvelope.id
|
||||
}`,
|
||||
documentName: document.title,
|
||||
documentName: createdEnvelope.title,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
|
||||
});
|
||||
|
||||
@ -600,8 +646,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
});
|
||||
|
||||
return {
|
||||
createdEnvelope,
|
||||
token: createdDirectRecipient.token,
|
||||
documentId: document.id,
|
||||
recipientId: createdDirectRecipient.id,
|
||||
};
|
||||
});
|
||||
@ -609,18 +655,21 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
try {
|
||||
// This handles sending emails and sealing the document if required.
|
||||
await sendDocument({
|
||||
documentId,
|
||||
userId: template.userId,
|
||||
teamId: template.teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: createdEnvelope.id,
|
||||
},
|
||||
userId: createdEnvelope.userId,
|
||||
teamId: createdEnvelope.teamId,
|
||||
requestMetadata,
|
||||
});
|
||||
|
||||
const createdDocument = await prisma.document.findFirstOrThrow({
|
||||
// Refetch envelope so we get the final data.
|
||||
const refetchedEnvelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
id: createdEnvelope.id,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
@ -628,9 +677,9 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_SIGNED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)),
|
||||
userId: template.userId,
|
||||
teamId: template.teamId ?? undefined,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(refetchedEnvelope)),
|
||||
userId: refetchedEnvelope.userId,
|
||||
teamId: refetchedEnvelope.teamId ?? undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[CREATE_DOCUMENT_FROM_DIRECT_TEMPLATE]:', err);
|
||||
@ -641,7 +690,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
return {
|
||||
token,
|
||||
documentId,
|
||||
documentId: incrementedDocumentId.documentId,
|
||||
recipientId,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,183 +0,0 @@
|
||||
import { DocumentSource, type RecipientRole } from '@prisma/client';
|
||||
|
||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
|
||||
export type CreateDocumentFromTemplateLegacyOptions = {
|
||||
templateId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
recipients?: {
|
||||
name?: string;
|
||||
email: string;
|
||||
role?: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
}[];
|
||||
};
|
||||
|
||||
// !TODO: Make this work
|
||||
|
||||
/**
|
||||
* Legacy server function for /api/v1
|
||||
*/
|
||||
export const createDocumentFromTemplateLegacy = async ({
|
||||
templateId,
|
||||
userId,
|
||||
teamId,
|
||||
recipients,
|
||||
}: CreateDocumentFromTemplateLegacyOptions) => {
|
||||
const template = await prisma.template.findUnique({
|
||||
where: {
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new Error('Template not found.');
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const documentData = await prisma.documentData.create({
|
||||
data: {
|
||||
type: template.templateDocumentData.type,
|
||||
data: template.templateDocumentData.data,
|
||||
initialData: template.templateDocumentData.initialData,
|
||||
},
|
||||
});
|
||||
|
||||
const recipientsToCreate = template.recipients.map((recipient) => ({
|
||||
id: recipient.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
}));
|
||||
|
||||
const document = await prisma.document.create({
|
||||
data: {
|
||||
qrToken: prefixedId('qr'),
|
||||
source: DocumentSource.TEMPLATE,
|
||||
templateId: template.id,
|
||||
userId,
|
||||
teamId: template.teamId,
|
||||
title: template.title,
|
||||
visibility: settings.documentVisibility,
|
||||
documentDataId: documentData.id,
|
||||
useLegacyFieldInsertion: template.useLegacyFieldInsertion ?? false,
|
||||
recipients: {
|
||||
create: recipientsToCreate.map((recipient) => ({
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: recipient.token,
|
||||
})),
|
||||
},
|
||||
documentMeta: {
|
||||
create: extractDerivedDocumentMeta(settings, template.templateMeta),
|
||||
},
|
||||
},
|
||||
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
documentData: true,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.field.createMany({
|
||||
data: template.fields.map((field) => {
|
||||
const recipient = recipientsToCreate.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
const documentRecipient = document.recipients.find(
|
||||
(documentRecipient) => documentRecipient.token === recipient?.token,
|
||||
);
|
||||
|
||||
if (!documentRecipient) {
|
||||
throw new Error('Recipient not found.');
|
||||
}
|
||||
|
||||
return {
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: field.customText,
|
||||
inserted: field.inserted,
|
||||
documentId: document.id,
|
||||
recipientId: documentRecipient.id,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
// Replicate the old logic, get by index and create if we exceed the number of existing recipients.
|
||||
if (recipients && recipients.length > 0) {
|
||||
await Promise.all(
|
||||
recipients.map(async (recipient, index) => {
|
||||
const existingRecipient = document.recipients.at(index);
|
||||
|
||||
if (existingRecipient) {
|
||||
return await prisma.recipient.update({
|
||||
where: {
|
||||
id: existingRecipient.id,
|
||||
documentId: document.id,
|
||||
},
|
||||
data: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.recipient.create({
|
||||
data: {
|
||||
documentId: document.id,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Gross but we need to do the additional fetch since we mutate above.
|
||||
const updatedRecipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...document,
|
||||
recipients: updatedRecipients,
|
||||
};
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
EnvelopeType,
|
||||
type Field,
|
||||
FolderType,
|
||||
type Recipient,
|
||||
@ -37,7 +38,7 @@ import {
|
||||
} from '../../types/field-meta';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
@ -47,7 +48,11 @@ import {
|
||||
createRecipientAuthOptions,
|
||||
extractDocumentAuthMethods,
|
||||
} from '../../utils/document-auth';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
import { incrementDocumentId } from '../envelope/increment-id';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
@ -60,7 +65,7 @@ type FinalRecipient = Pick<
|
||||
};
|
||||
|
||||
export type CreateDocumentFromTemplateOptions = {
|
||||
templateId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
externalId?: string | null;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
@ -268,7 +273,7 @@ const getUpdatedFieldMeta = (field: Field, prefillField?: TFieldMetaPrefillField
|
||||
};
|
||||
|
||||
export const createDocumentFromTemplate = async ({
|
||||
templateId,
|
||||
id,
|
||||
externalId,
|
||||
userId,
|
||||
teamId,
|
||||
@ -279,19 +284,27 @@ export const createDocumentFromTemplate = async ({
|
||||
folderId,
|
||||
prefillFields,
|
||||
}: CreateDocumentFromTemplateOptions) => {
|
||||
const template = await prisma.template.findUnique({
|
||||
where: {
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const template = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: {
|
||||
include: {
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -317,6 +330,15 @@ export const createDocumentFromTemplate = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const legacyTemplateId = mapSecondaryIdToTemplateId(template.secondaryId);
|
||||
|
||||
// Todo: Envelopes
|
||||
if (template.envelopeItems.length !== 1) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Template must have exactly 1 envelope item',
|
||||
});
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
@ -354,7 +376,8 @@ export const createDocumentFromTemplate = async ({
|
||||
};
|
||||
});
|
||||
|
||||
let parentDocumentData = template.templateDocumentData;
|
||||
// Todo: Envelopes
|
||||
let parentDocumentData = template.envelopeItems[0].documentData;
|
||||
|
||||
if (customDocumentDataId) {
|
||||
const customDocumentData = await prisma.documentData.findFirst({
|
||||
@ -380,48 +403,58 @@ export const createDocumentFromTemplate = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const incrementedDocumentId = await incrementDocumentId();
|
||||
|
||||
const documentMeta = await prisma.documentMeta.create({
|
||||
data: extractDerivedDocumentMeta(settings, {
|
||||
subject: override?.subject || template.documentMeta?.subject,
|
||||
message: override?.message || template.documentMeta?.message,
|
||||
timezone: override?.timezone || template.documentMeta?.timezone,
|
||||
dateFormat: override?.dateFormat || template.documentMeta?.dateFormat,
|
||||
redirectUrl: override?.redirectUrl || template.documentMeta?.redirectUrl,
|
||||
distributionMethod: override?.distributionMethod || template.documentMeta?.distributionMethod,
|
||||
emailSettings: override?.emailSettings || template.documentMeta?.emailSettings,
|
||||
signingOrder: override?.signingOrder || template.documentMeta?.signingOrder,
|
||||
language: override?.language || template.documentMeta?.language || settings.documentLanguage,
|
||||
typedSignatureEnabled:
|
||||
override?.typedSignatureEnabled ?? template.documentMeta?.typedSignatureEnabled,
|
||||
uploadSignatureEnabled:
|
||||
override?.uploadSignatureEnabled ?? template.documentMeta?.uploadSignatureEnabled,
|
||||
drawSignatureEnabled:
|
||||
override?.drawSignatureEnabled ?? template.documentMeta?.drawSignatureEnabled,
|
||||
allowDictateNextSigner:
|
||||
override?.allowDictateNextSigner ?? template.documentMeta?.allowDictateNextSigner,
|
||||
}),
|
||||
});
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.create({
|
||||
const envelope = await tx.envelope.create({
|
||||
data: {
|
||||
id: prefixedId('envelope'),
|
||||
secondaryId: incrementedDocumentId.formattedDocumentId,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
qrToken: prefixedId('qr'),
|
||||
source: DocumentSource.TEMPLATE,
|
||||
externalId: externalId || template.externalId,
|
||||
templateId: template.id,
|
||||
templateId: legacyTemplateId, // The template this envelope was created from.
|
||||
userId,
|
||||
folderId,
|
||||
teamId: template.teamId,
|
||||
title: override?.title || template.title,
|
||||
documentDataId: documentData.id,
|
||||
envelopeItems: {
|
||||
create: {
|
||||
id: prefixedId('envelope_item'),
|
||||
title: override?.title || template.title,
|
||||
documentDataId: documentData.id,
|
||||
},
|
||||
},
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: templateAuthOptions.globalAccessAuth,
|
||||
globalActionAuth: templateAuthOptions.globalActionAuth,
|
||||
}),
|
||||
visibility: template.visibility || settings.documentVisibility,
|
||||
useLegacyFieldInsertion: template.useLegacyFieldInsertion ?? false,
|
||||
documentMeta: {
|
||||
create: extractDerivedDocumentMeta(settings, {
|
||||
subject: override?.subject || template.templateMeta?.subject,
|
||||
message: override?.message || template.templateMeta?.message,
|
||||
timezone: override?.timezone || template.templateMeta?.timezone,
|
||||
password: override?.password || template.templateMeta?.password,
|
||||
dateFormat: override?.dateFormat || template.templateMeta?.dateFormat,
|
||||
redirectUrl: override?.redirectUrl || template.templateMeta?.redirectUrl,
|
||||
distributionMethod:
|
||||
override?.distributionMethod || template.templateMeta?.distributionMethod,
|
||||
emailSettings: override?.emailSettings || template.templateMeta?.emailSettings,
|
||||
signingOrder: override?.signingOrder || template.templateMeta?.signingOrder,
|
||||
language:
|
||||
override?.language || template.templateMeta?.language || settings.documentLanguage,
|
||||
typedSignatureEnabled:
|
||||
override?.typedSignatureEnabled ?? template.templateMeta?.typedSignatureEnabled,
|
||||
uploadSignatureEnabled:
|
||||
override?.uploadSignatureEnabled ?? template.templateMeta?.uploadSignatureEnabled,
|
||||
drawSignatureEnabled:
|
||||
override?.drawSignatureEnabled ?? template.templateMeta?.drawSignatureEnabled,
|
||||
allowDictateNextSigner:
|
||||
override?.allowDictateNextSigner ?? template.templateMeta?.allowDictateNextSigner,
|
||||
}),
|
||||
},
|
||||
documentMetaId: documentMeta.id,
|
||||
recipients: {
|
||||
createMany: {
|
||||
data: finalRecipients.map((recipient) => {
|
||||
@ -454,11 +487,17 @@ export const createDocumentFromTemplate = async ({
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
documentData: true,
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let fieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
|
||||
const envelopeItemId = envelope.envelopeItems[0].id;
|
||||
|
||||
let fieldsToCreate: Omit<Field, 'id' | 'secondaryId'>[] = [];
|
||||
|
||||
// Get all template field IDs first so we can validate later
|
||||
const allTemplateFieldIds = finalRecipients.flatMap((recipient) =>
|
||||
@ -502,7 +541,7 @@ export const createDocumentFromTemplate = async ({
|
||||
}
|
||||
|
||||
Object.values(finalRecipients).forEach(({ token, fields }) => {
|
||||
const recipient = document.recipients.find((recipient) => recipient.token === token);
|
||||
const recipient = envelope.recipients.find((recipient) => recipient.token === token);
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found.');
|
||||
@ -513,7 +552,8 @@ export const createDocumentFromTemplate = async ({
|
||||
const prefillField = prefillFields?.find((value) => value.id === field.id);
|
||||
|
||||
const payload = {
|
||||
documentId: document.id,
|
||||
envelopeItemId,
|
||||
envelopeId: envelope.id, // Todo: Envelopes
|
||||
recipientId: recipient.id,
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
@ -544,7 +584,7 @@ export const createDocumentFromTemplate = async ({
|
||||
}
|
||||
|
||||
payload.customText = DateTime.fromJSDate(date).toFormat(
|
||||
template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
template.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
);
|
||||
|
||||
payload.inserted = true;
|
||||
@ -569,21 +609,21 @@ export const createDocumentFromTemplate = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
title: document.title,
|
||||
title: envelope.title,
|
||||
source: {
|
||||
type: DocumentSource.TEMPLATE,
|
||||
templateId: template.id,
|
||||
templateId: legacyTemplateId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const createdDocument = await tx.document.findFirst({
|
||||
const createdEnvelope = await tx.envelope.findFirst({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
@ -591,17 +631,17 @@ export const createDocumentFromTemplate = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdDocument) {
|
||||
if (!createdEnvelope) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)),
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return document;
|
||||
return envelope;
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { EnvelopeType, type Recipient } from '@prisma/client';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import {
|
||||
@ -8,50 +8,55 @@ import {
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { type EnvelopeIdOptions, mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type CreateTemplateDirectLinkOptions = {
|
||||
templateId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
directRecipientId?: number;
|
||||
};
|
||||
|
||||
export const createTemplateDirectLink = async ({
|
||||
templateId,
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
directRecipientId,
|
||||
}: CreateTemplateDirectLinkOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
directLink: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Template not found' });
|
||||
}
|
||||
|
||||
if (template.directLink) {
|
||||
if (envelope.directLink) {
|
||||
throw new AppError(AppErrorCode.ALREADY_EXISTS, { message: 'Direct template already exists' });
|
||||
}
|
||||
|
||||
if (
|
||||
directRecipientId &&
|
||||
!template.recipients.find((recipient) => recipient.id === directRecipientId)
|
||||
!envelope.recipients.find((recipient) => recipient.id === directRecipientId)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Recipient not found' });
|
||||
}
|
||||
|
||||
if (
|
||||
!directRecipientId &&
|
||||
template.recipients.find(
|
||||
envelope.recipients.find(
|
||||
(recipient) => recipient.email.toLowerCase() === DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||
)
|
||||
) {
|
||||
@ -60,13 +65,13 @@ export const createTemplateDirectLink = async ({
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const createdDirectLink = await prisma.$transaction(async (tx) => {
|
||||
let recipient: Recipient | undefined;
|
||||
|
||||
if (directRecipientId) {
|
||||
recipient = await tx.recipient.update({
|
||||
where: {
|
||||
templateId,
|
||||
envelopeId: envelope.id,
|
||||
id: directRecipientId,
|
||||
},
|
||||
data: {
|
||||
@ -77,7 +82,7 @@ export const createTemplateDirectLink = async ({
|
||||
} else {
|
||||
recipient = await tx.recipient.create({
|
||||
data: {
|
||||
templateId,
|
||||
envelopeId: envelope.id,
|
||||
name: DIRECT_TEMPLATE_RECIPIENT_NAME,
|
||||
email: DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||
token: nanoid(),
|
||||
@ -87,11 +92,20 @@ export const createTemplateDirectLink = async ({
|
||||
|
||||
return await tx.templateDirectLink.create({
|
||||
data: {
|
||||
templateId,
|
||||
envelopeId: envelope.id,
|
||||
enabled: true,
|
||||
token: nanoid(),
|
||||
directTemplateRecipientId: recipient.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
id: createdDirectLink.id,
|
||||
token: createdDirectLink.token,
|
||||
createdAt: createdDirectLink.createdAt,
|
||||
enabled: createdDirectLink.enabled,
|
||||
directTemplateRecipientId: createdDirectLink.directTemplateRecipientId,
|
||||
templateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,112 +0,0 @@
|
||||
import type { DocumentMeta, DocumentVisibility, Template } from '@prisma/client';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema//TemplateSchema';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import { createDocumentAuthOptions } from '../../utils/document-auth';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
|
||||
export type CreateTemplateOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
templateDocumentDataId: string;
|
||||
data: {
|
||||
title: string;
|
||||
folderId?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
publicTitle?: string;
|
||||
publicDescription?: string;
|
||||
type?: Template['type'];
|
||||
};
|
||||
meta?: Partial<Omit<DocumentMeta, 'id' | 'templateId'>>;
|
||||
};
|
||||
|
||||
export const ZCreateTemplateResponseSchema = TemplateSchema;
|
||||
|
||||
export type TCreateTemplateResponse = z.infer<typeof ZCreateTemplateResponseSchema>;
|
||||
|
||||
export const createTemplate = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateDocumentDataId,
|
||||
data,
|
||||
meta = {},
|
||||
}: CreateTemplateOptions) => {
|
||||
const { title, folderId } = data;
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({ teamId, userId }),
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
if (folderId) {
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: folderId,
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const emailId = meta.emailId;
|
||||
|
||||
// Validate that the email ID belongs to the organisation.
|
||||
if (emailId) {
|
||||
const email = await prisma.organisationEmail.findFirst({
|
||||
where: {
|
||||
id: emailId,
|
||||
organisationId: team.organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Email not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.template.create({
|
||||
data: {
|
||||
title,
|
||||
teamId,
|
||||
userId,
|
||||
templateDocumentDataId,
|
||||
folderId,
|
||||
externalId: data.externalId,
|
||||
visibility: data.visibility ?? settings.documentVisibility,
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: data.globalAccessAuth || [],
|
||||
globalActionAuth: data.globalActionAuth || [],
|
||||
}),
|
||||
publicTitle: data.publicTitle,
|
||||
publicDescription: data.publicDescription,
|
||||
type: data.type,
|
||||
templateMeta: {
|
||||
create: extractDerivedDocumentMeta(settings, meta),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,8 +1,10 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { generateAvaliableRecipientPlaceholder } from '@documenso/lib/utils/templates';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type DeleteTemplateDirectLinkOptions = {
|
||||
templateId: number;
|
||||
@ -15,24 +17,31 @@ export const deleteTemplateDirectLink = async ({
|
||||
userId,
|
||||
teamId,
|
||||
}: DeleteTemplateDirectLinkOptions): Promise<void> => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
directLink: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
const { directLink } = template;
|
||||
const { directLink } = envelope;
|
||||
|
||||
if (!directLink) {
|
||||
return;
|
||||
@ -41,17 +50,17 @@ export const deleteTemplateDirectLink = async ({
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.recipient.update({
|
||||
where: {
|
||||
templateId: template.id,
|
||||
envelopeId: envelope.id,
|
||||
id: directLink.directTemplateRecipientId,
|
||||
},
|
||||
data: {
|
||||
...generateAvaliableRecipientPlaceholder(template.recipients),
|
||||
...generateAvaliableRecipientPlaceholder(envelope.recipients),
|
||||
},
|
||||
});
|
||||
|
||||
await tx.templateDirectLink.delete({
|
||||
where: {
|
||||
templateId,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type DeleteTemplateOptions = {
|
||||
id: number;
|
||||
@ -9,10 +11,17 @@ export type DeleteTemplateOptions = {
|
||||
};
|
||||
|
||||
export const deleteTemplate = async ({ id, userId, teamId }: DeleteTemplateOptions) => {
|
||||
return await prisma.template.delete({
|
||||
where: {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return await prisma.envelope.delete({
|
||||
where: envelopeWhereInput,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,27 +1,30 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { DocumentSource, EnvelopeType } from '@prisma/client';
|
||||
import { omit } from 'remeda';
|
||||
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { TDuplicateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { type EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
import { incrementTemplateId } from '../envelope/increment-id';
|
||||
|
||||
export type DuplicateTemplateOptions = TDuplicateTemplateMutationSchema & {
|
||||
export type DuplicateTemplateOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
};
|
||||
|
||||
export const duplicateTemplate = async ({
|
||||
templateId,
|
||||
userId,
|
||||
teamId,
|
||||
}: DuplicateTemplateOptions) => {
|
||||
const template = await prisma.template.findUnique({
|
||||
where: {
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
export const duplicateTemplate = async ({ id, userId, teamId }: DuplicateTemplateOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: {
|
||||
select: {
|
||||
@ -32,79 +35,105 @@ export const duplicateTemplate = async ({
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new Error('Template not found.');
|
||||
}
|
||||
|
||||
const documentData = await prisma.documentData.create({
|
||||
data: {
|
||||
type: template.templateDocumentData.type,
|
||||
data: template.templateDocumentData.data,
|
||||
initialData: template.templateDocumentData.initialData,
|
||||
},
|
||||
});
|
||||
|
||||
let templateMeta: Prisma.TemplateCreateArgs['data']['templateMeta'] | undefined = undefined;
|
||||
|
||||
if (template.templateMeta) {
|
||||
templateMeta = {
|
||||
create: {
|
||||
...omit(template.templateMeta, ['id', 'templateId']),
|
||||
emailSettings: template.templateMeta.emailSettings || undefined,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
const duplicatedTemplate = await prisma.template.create({
|
||||
const { formattedTemplateId } = await incrementTemplateId();
|
||||
|
||||
const createdDocumentMeta = await prisma.documentMeta.create({
|
||||
data: {
|
||||
...omit(envelope.documentMeta, ['id']),
|
||||
emailSettings: envelope.documentMeta.emailSettings || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const duplicatedEnvelope = await prisma.envelope.create({
|
||||
data: {
|
||||
id: prefixedId('envelope'),
|
||||
secondaryId: formattedTemplateId,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
title: template.title + ' (copy)',
|
||||
templateDocumentDataId: documentData.id,
|
||||
authOptions: template.authOptions || undefined,
|
||||
visibility: template.visibility,
|
||||
templateMeta,
|
||||
title: envelope.title + ' (copy)',
|
||||
documentMetaId: createdDocumentMeta.id,
|
||||
authOptions: envelope.authOptions || undefined,
|
||||
visibility: envelope.visibility,
|
||||
source: DocumentSource.DOCUMENT, // Todo: Migration what to use here.
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
const recipientsToCreate = template.recipients.map((recipient) => ({
|
||||
templateId: duplicatedTemplate.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
fields: {
|
||||
createMany: {
|
||||
data: recipient.fields.map((field) => ({
|
||||
templateId: duplicatedTemplate.id,
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
|
||||
})),
|
||||
},
|
||||
},
|
||||
}));
|
||||
// Key = original envelope item ID
|
||||
// Value = duplicated envelope item ID.
|
||||
const oldEnvelopeItemToNewEnvelopeItemIdMap: Record<string, string> = {};
|
||||
|
||||
for (const recipientData of recipientsToCreate) {
|
||||
// Duplicate the envelope items.
|
||||
await Promise.all(
|
||||
envelope.envelopeItems.map(async (envelopeItem) => {
|
||||
const duplicatedDocumentData = await prisma.documentData.create({
|
||||
data: {
|
||||
type: envelopeItem.documentData.type,
|
||||
data: envelopeItem.documentData.initialData,
|
||||
initialData: envelopeItem.documentData.initialData,
|
||||
},
|
||||
});
|
||||
|
||||
const duplicatedEnvelopeItem = await prisma.envelopeItem.create({
|
||||
data: {
|
||||
id: prefixedId('envelope_item'),
|
||||
title: envelopeItem.title,
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
documentDataId: duplicatedDocumentData.id,
|
||||
},
|
||||
});
|
||||
|
||||
oldEnvelopeItemToNewEnvelopeItemIdMap[envelopeItem.id] = duplicatedEnvelopeItem.id;
|
||||
}),
|
||||
);
|
||||
|
||||
for (const recipient of envelope.recipients) {
|
||||
await prisma.recipient.create({
|
||||
data: recipientData,
|
||||
data: {
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
fields: {
|
||||
createMany: {
|
||||
data: recipient.fields.map((field) => ({
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId],
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return duplicatedTemplate;
|
||||
return duplicatedEnvelope;
|
||||
};
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import { DocumentVisibility, type Prisma, TeamMemberRole, type Template } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
import type { TemplateType } from '@prisma/client';
|
||||
import { EnvelopeType, type Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { type FindResultResponse } from '../../types/search-params';
|
||||
import { getMemberRoles } from '../team/get-member-roles';
|
||||
|
||||
export type FindTemplatesOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
type?: Template['type'];
|
||||
type?: TemplateType;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
folderId?: string;
|
||||
@ -23,46 +24,29 @@ export const findTemplates = async ({
|
||||
perPage = 10,
|
||||
folderId,
|
||||
}: FindTemplatesOptions) => {
|
||||
const whereFilter: Prisma.TemplateWhereInput[] = [];
|
||||
const whereFilter: Prisma.EnvelopeWhereInput[] = [];
|
||||
|
||||
if (teamId === undefined) {
|
||||
whereFilter.push({ userId });
|
||||
}
|
||||
const { teamRole } = await getMemberRoles({
|
||||
teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (teamId !== undefined) {
|
||||
const { teamRole } = await getMemberRoles({
|
||||
teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
whereFilter.push(
|
||||
{ teamId },
|
||||
{
|
||||
OR: [
|
||||
match(teamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => ({
|
||||
visibility: {
|
||||
in: [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
DocumentVisibility.ADMIN,
|
||||
],
|
||||
},
|
||||
}))
|
||||
.with(TeamMemberRole.MANAGER, () => ({
|
||||
visibility: {
|
||||
in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE],
|
||||
},
|
||||
}))
|
||||
.otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })),
|
||||
{ userId, teamId },
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
whereFilter.push(
|
||||
{ teamId },
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[teamRole],
|
||||
},
|
||||
},
|
||||
{ userId, teamId },
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
if (folderId) {
|
||||
whereFilter.push({ folderId });
|
||||
@ -71,9 +55,10 @@ export const findTemplates = async ({
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.template.findMany({
|
||||
prisma.envelope.findMany({
|
||||
where: {
|
||||
type,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: type,
|
||||
AND: whereFilter,
|
||||
},
|
||||
include: {
|
||||
@ -85,7 +70,7 @@ export const findTemplates = async ({
|
||||
},
|
||||
fields: true,
|
||||
recipients: true,
|
||||
templateMeta: true,
|
||||
documentMeta: true,
|
||||
directLink: {
|
||||
select: {
|
||||
token: true,
|
||||
@ -98,8 +83,10 @@ export const findTemplates = async ({
|
||||
createdAt: 'desc',
|
||||
},
|
||||
}),
|
||||
prisma.template.count({
|
||||
prisma.envelope.count({
|
||||
where: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: type,
|
||||
AND: whereFilter,
|
||||
},
|
||||
}),
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
|
||||
export interface GetTemplateByDirectLinkTokenOptions {
|
||||
token: string;
|
||||
@ -9,8 +12,9 @@ export interface GetTemplateByDirectLinkTokenOptions {
|
||||
export const getTemplateByDirectLinkToken = async ({
|
||||
token,
|
||||
}: GetTemplateByDirectLinkTokenOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
directLink: {
|
||||
token,
|
||||
enabled: true,
|
||||
@ -23,21 +27,53 @@ export const getTemplateByDirectLinkToken = async ({
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
const directLink = template?.directLink;
|
||||
const directLink = envelope?.directLink;
|
||||
|
||||
// Todo: Envelopes
|
||||
const firstDocumentData = envelope?.envelopeItems[0]?.documentData;
|
||||
|
||||
// Doing this to enforce type safety for directLink.
|
||||
if (!directLink) {
|
||||
if (!directLink || !firstDocumentData) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const recipientsWithMappedFields = envelope.recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
fields: recipient.fields.map((field) => ({
|
||||
...field,
|
||||
templateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
documentId: undefined,
|
||||
})),
|
||||
}));
|
||||
|
||||
// Backwards compatibility mapping.
|
||||
return {
|
||||
...template,
|
||||
id: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
type: envelope.templateType,
|
||||
visibility: envelope.visibility,
|
||||
externalId: envelope.externalId,
|
||||
title: envelope.title,
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
authOptions: envelope.authOptions,
|
||||
createdAt: envelope.createdAt,
|
||||
updatedAt: envelope.updatedAt,
|
||||
publicTitle: envelope.publicTitle,
|
||||
publicDescription: envelope.publicDescription,
|
||||
folderId: envelope.folderId,
|
||||
templateDocumentData: firstDocumentData,
|
||||
directLink,
|
||||
fields: template.recipients.map((recipient) => recipient.fields).flat(),
|
||||
templateMeta: envelope.documentMeta,
|
||||
recipients: recipientsWithMappedFields,
|
||||
fields: recipientsWithMappedFields.flatMap((recipient) => recipient.fields),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,31 +1,38 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type GetTemplateByIdOptions = {
|
||||
id: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
folderId?: string | null;
|
||||
};
|
||||
|
||||
export const getTemplateById = async ({
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
folderId = null,
|
||||
}: GetTemplateByIdOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
...(folderId ? { folderId } : {}),
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
directLink: true,
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
documentMeta: true,
|
||||
envelopeItems: {
|
||||
select: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
fields: true,
|
||||
user: {
|
||||
@ -39,11 +46,35 @@ export const getTemplateById = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
return template;
|
||||
// Todo: Envelopes
|
||||
const firstTemplateDocumentData = envelope.envelopeItems[0].documentData;
|
||||
|
||||
if (!firstTemplateDocumentData) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template document data not found',
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
const { envelopeItems, documentMeta, ...rest } = envelope;
|
||||
|
||||
const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
type: envelope.templateType,
|
||||
templateDocumentData: firstTemplateDocumentData,
|
||||
templateMeta: envelope.documentMeta,
|
||||
fields: envelope.fields.map((field) => ({
|
||||
...field,
|
||||
templateId: legacyTemplateId,
|
||||
})),
|
||||
id: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type ToggleTemplateDirectLinkOptions = {
|
||||
templateId: number;
|
||||
@ -16,24 +19,31 @@ export const toggleTemplateDirectLink = async ({
|
||||
teamId,
|
||||
enabled,
|
||||
}: ToggleTemplateDirectLinkOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
directLink: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
const { directLink } = template;
|
||||
const { directLink } = envelope;
|
||||
|
||||
if (!directLink) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
@ -41,13 +51,22 @@ export const toggleTemplateDirectLink = async ({
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.templateDirectLink.update({
|
||||
const updatedDirectLink = await prisma.templateDirectLink.update({
|
||||
where: {
|
||||
id: directLink.id,
|
||||
},
|
||||
data: {
|
||||
templateId: template.id,
|
||||
envelopeId: envelope.id,
|
||||
enabled,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: updatedDirectLink.id,
|
||||
token: updatedDirectLink.token,
|
||||
createdAt: updatedDirectLink.createdAt,
|
||||
enabled: updatedDirectLink.enabled,
|
||||
directTemplateRecipientId: updatedDirectLink.directTemplateRecipientId,
|
||||
templateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,11 +1,19 @@
|
||||
import type { DocumentMeta, DocumentVisibility, Template } from '@prisma/client';
|
||||
import type { Prisma, TemplateType } from '@prisma/client';
|
||||
import {
|
||||
type DocumentMeta,
|
||||
type DocumentVisibility,
|
||||
EnvelopeType,
|
||||
FolderType,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type UpdateTemplateOptions = {
|
||||
userId: number;
|
||||
@ -13,13 +21,14 @@ export type UpdateTemplateOptions = {
|
||||
templateId: number;
|
||||
data?: {
|
||||
title?: string;
|
||||
folderId?: string | null;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
publicTitle?: string;
|
||||
publicDescription?: string;
|
||||
type?: Template['type'];
|
||||
type?: TemplateType;
|
||||
useLegacyFieldInsertion?: boolean;
|
||||
};
|
||||
meta?: Partial<Omit<DocumentMeta, 'id' | 'templateId'>>;
|
||||
@ -32,13 +41,20 @@ export const updateTemplate = async ({
|
||||
meta = {},
|
||||
data = {},
|
||||
}: UpdateTemplateOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
const { envelopeWhereInput, team } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
templateMeta: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
organisationId: true,
|
||||
@ -52,18 +68,18 @@ export const updateTemplate = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.values(data).length === 0 && Object.keys(meta).length === 0) {
|
||||
return template;
|
||||
return envelope;
|
||||
}
|
||||
|
||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||
documentAuth: template.authOptions,
|
||||
documentAuth: envelope.authOptions,
|
||||
});
|
||||
|
||||
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
|
||||
@ -76,7 +92,7 @@ export const updateTemplate = async ({
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth.length > 0 && !template.team.organisation.organisationClaim.flags.cfr21) {
|
||||
if (newGlobalActionAuth.length > 0 && !envelope.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
@ -94,7 +110,7 @@ export const updateTemplate = async ({
|
||||
const email = await prisma.organisationEmail.findFirst({
|
||||
where: {
|
||||
id: emailId,
|
||||
organisationId: template.team.organisationId,
|
||||
organisationId: envelope.team.organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
@ -105,32 +121,63 @@ export const updateTemplate = async ({
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.template.update({
|
||||
let folderUpdateQuery: Prisma.FolderUpdateOneWithoutEnvelopesNestedInput | undefined = undefined;
|
||||
|
||||
// Validate folder ID.
|
||||
if (data.folderId) {
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: data.folderId,
|
||||
team: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
}),
|
||||
type: FolderType.TEMPLATE,
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
|
||||
folderUpdateQuery = {
|
||||
connect: {
|
||||
id: data.folderId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Move template to root folder if folderId is null.
|
||||
if (data.folderId === null) {
|
||||
folderUpdateQuery = {
|
||||
disconnect: true,
|
||||
};
|
||||
}
|
||||
|
||||
return await prisma.envelope.update({
|
||||
where: {
|
||||
id: templateId,
|
||||
id: envelope.id,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
},
|
||||
data: {
|
||||
templateType: data?.type,
|
||||
title: data?.title,
|
||||
externalId: data?.externalId,
|
||||
type: data?.type,
|
||||
visibility: data?.visibility,
|
||||
publicDescription: data?.publicDescription,
|
||||
publicTitle: data?.publicTitle,
|
||||
useLegacyFieldInsertion: data?.useLegacyFieldInsertion,
|
||||
folder: folderUpdateQuery,
|
||||
authOptions,
|
||||
templateMeta: {
|
||||
upsert: {
|
||||
where: {
|
||||
templateId,
|
||||
},
|
||||
create: {
|
||||
...meta,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
},
|
||||
update: {
|
||||
...meta,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
},
|
||||
documentMeta: {
|
||||
update: {
|
||||
...meta,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -25,9 +25,10 @@ export const deleteUser = async ({ id }: DeleteUserOptions) => {
|
||||
const serviceAccount = await deletedAccountServiceAccount();
|
||||
|
||||
// TODO: Send out cancellations for all pending docs
|
||||
await prisma.document.updateMany({
|
||||
await prisma.envelope.updateMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
status: {
|
||||
in: [DocumentStatus.PENDING, DocumentStatus.REJECTED, DocumentStatus.COMPLETED],
|
||||
},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { EnvelopeType, Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -34,12 +34,20 @@ export const findUsers = async ({
|
||||
|
||||
const [users, count] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
include: {
|
||||
documents: {
|
||||
select: {
|
||||
_count: {
|
||||
select: {
|
||||
id: true,
|
||||
envelopes: {
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
roles: true,
|
||||
},
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
@ -51,7 +59,10 @@ export const findUsers = async ({
|
||||
]);
|
||||
|
||||
return {
|
||||
users,
|
||||
users: users.map((user) => ({
|
||||
...user,
|
||||
documentCount: user._count.envelopes,
|
||||
})),
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
};
|
||||
};
|
||||
|
||||
@ -410,7 +410,6 @@ export const generateSampleWebhookPayload = (
|
||||
externalId: null,
|
||||
userId: 3,
|
||||
status: DocumentStatus.PENDING,
|
||||
documentDataId: 'cm6exvn93006hi02ru90a265a',
|
||||
documentMeta: {
|
||||
...basePayload.documentMeta,
|
||||
id: 'cm6exvn96006ji02rqvzjvwoy',
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { Webhook } from '@prisma/client';
|
||||
import { EnvelopeType, type Webhook } from '@prisma/client';
|
||||
|
||||
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
|
||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { mapSecondaryIdToDocumentId } from '../../../utils/envelope';
|
||||
import { getWebhooksByTeamId } from '../get-webhooks-by-team-id';
|
||||
import { validateApiToken } from './validateApiToken';
|
||||
|
||||
@ -14,48 +14,73 @@ export const listDocumentsHandler = async (req: Request) => {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const { user, userId, teamId } = await validateApiToken({ authorization });
|
||||
const { user, teamId } = await validateApiToken({ authorization });
|
||||
|
||||
let allWebhooks: Webhook[] = [];
|
||||
const allWebhooks: Webhook[] = await getWebhooksByTeamId(teamId, user.id);
|
||||
|
||||
const documents = await findDocuments({
|
||||
userId: userId ?? user.id,
|
||||
teamId,
|
||||
perPage: 1,
|
||||
});
|
||||
|
||||
const recipients = await getRecipientsForDocument({
|
||||
documentId: documents.data[0].id,
|
||||
userId: userId ?? user.id,
|
||||
teamId,
|
||||
});
|
||||
|
||||
allWebhooks = await getWebhooksByTeamId(teamId, user.id);
|
||||
|
||||
if (documents && documents.data.length > 0 && allWebhooks.length > 0 && recipients.length > 0) {
|
||||
const testWebhook = {
|
||||
event: allWebhooks[0].eventTriggers.toString(),
|
||||
createdAt: allWebhooks[0].createdAt,
|
||||
webhookEndpoint: allWebhooks[0].webhookUrl,
|
||||
payload: {
|
||||
id: documents.data[0].id,
|
||||
userId: documents.data[0].userId,
|
||||
title: documents.data[0].title,
|
||||
status: documents.data[0].status,
|
||||
documentDataId: documents.data[0].documentDataId,
|
||||
createdAt: documents.data[0].createdAt,
|
||||
updatedAt: documents.data[0].updatedAt,
|
||||
completedAt: documents.data[0].completedAt,
|
||||
deletedAt: documents.data[0].deletedAt,
|
||||
teamId: documents.data[0].teamId,
|
||||
Recipient: recipients,
|
||||
const document = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
teamId,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
include: {
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.json([testWebhook]);
|
||||
if (
|
||||
!document ||
|
||||
document.envelopeItems.length === 0 ||
|
||||
document.recipients.length === 0 ||
|
||||
allWebhooks.length === 0
|
||||
) {
|
||||
return Response.json([]);
|
||||
}
|
||||
|
||||
return Response.json([]);
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(document.secondaryId);
|
||||
|
||||
const testWebhook = {
|
||||
event: allWebhooks[0].eventTriggers.toString(),
|
||||
createdAt: allWebhooks[0].createdAt,
|
||||
webhookEndpoint: allWebhooks[0].webhookUrl,
|
||||
payload: {
|
||||
id: legacyDocumentId,
|
||||
userId: document.userId,
|
||||
title: document.title,
|
||||
status: document.status,
|
||||
createdAt: document.createdAt,
|
||||
updatedAt: document.updatedAt,
|
||||
completedAt: document.completedAt,
|
||||
deletedAt: document.deletedAt,
|
||||
teamId: document.teamId,
|
||||
Recipient: document.recipients.map((recipient) => ({
|
||||
id: recipient.id,
|
||||
documentId: legacyDocumentId,
|
||||
templateId: null,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
token: recipient.token,
|
||||
documentDeletedAt: recipient.documentDeletedAt,
|
||||
expired: recipient.expired,
|
||||
signedAt: recipient.signedAt,
|
||||
authOptions: recipient.authOptions,
|
||||
signingOrder: recipient.signingOrder,
|
||||
rejectionReason: recipient.rejectionReason,
|
||||
role: recipient.role,
|
||||
readStatus: recipient.readStatus,
|
||||
signingStatus: recipient.signingStatus,
|
||||
sendStatus: recipient.sendStatus,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
return Response.json([testWebhook]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user