mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
feat: optional email sending for api users
Introduces the ability to not send an email when sending (publishing) a document using the API. Additionally returns the signing link for each recipient when working with recipient API endpoints and returns the document object including recipients when sending documents via API.
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import { createNextRoute } from '@ts-rest/next';
|
||||
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
|
||||
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
||||
@ -76,7 +77,10 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
status: 200,
|
||||
body: {
|
||||
...document,
|
||||
recipients,
|
||||
recipients: recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
||||
})),
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
@ -258,6 +262,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
email: recipient.email,
|
||||
token: recipient.token,
|
||||
role: recipient.role,
|
||||
|
||||
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
||||
})),
|
||||
},
|
||||
};
|
||||
@ -349,6 +355,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
email: recipient.email,
|
||||
token: recipient.token,
|
||||
role: recipient.role,
|
||||
|
||||
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
||||
})),
|
||||
},
|
||||
};
|
||||
@ -428,6 +436,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
email: recipient.email,
|
||||
token: recipient.token,
|
||||
role: recipient.role,
|
||||
|
||||
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
||||
})),
|
||||
},
|
||||
};
|
||||
@ -435,6 +445,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
|
||||
sendDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id } = args.params;
|
||||
const { sendEmail = true } = args.body ?? {};
|
||||
|
||||
const document = await getDocumentById({ id: Number(id), userId: user.id, teamId: team?.id });
|
||||
|
||||
@ -490,10 +501,11 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
// });
|
||||
// }
|
||||
|
||||
await sendDocument({
|
||||
const { Recipient: recipients, ...sentDocument } = await sendDocument({
|
||||
documentId: Number(id),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
sendEmail,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
|
||||
@ -501,6 +513,11 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'Document sent for signing successfully',
|
||||
...sentDocument,
|
||||
recipients: recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
||||
})),
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
@ -585,6 +602,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
body: {
|
||||
...newRecipient,
|
||||
documentId: Number(documentId),
|
||||
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${newRecipient.token}`,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
@ -650,6 +668,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
body: {
|
||||
...updatedRecipient,
|
||||
documentId: Number(documentId),
|
||||
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${updatedRecipient.token}`,
|
||||
},
|
||||
};
|
||||
}),
|
||||
@ -703,6 +722,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
body: {
|
||||
...deletedRecipient,
|
||||
documentId: Number(documentId),
|
||||
signingUrl: '',
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
@ -45,7 +45,11 @@ export type TSuccessfulGetDocumentResponseSchema = z.infer<
|
||||
|
||||
export type TSuccessfulDocumentResponseSchema = z.infer<typeof ZSuccessfulDocumentResponseSchema>;
|
||||
|
||||
export const ZSendDocumentForSigningMutationSchema = null;
|
||||
export const ZSendDocumentForSigningMutationSchema = z
|
||||
.object({
|
||||
sendEmail: z.boolean().optional().default(true),
|
||||
})
|
||||
.or(z.literal('').transform(() => ({ sendEmail: true })));
|
||||
|
||||
export type TSendDocumentForSigningMutationSchema = typeof ZSendDocumentForSigningMutationSchema;
|
||||
|
||||
@ -89,8 +93,12 @@ export const ZCreateDocumentMutationResponseSchema = z.object({
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
recipientId: z.number(),
|
||||
name: z.string(),
|
||||
email: z.string().email().min(1),
|
||||
token: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
|
||||
signingUrl: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
@ -134,6 +142,8 @@ export const ZCreateDocumentFromTemplateMutationResponseSchema = z.object({
|
||||
email: z.string().email().min(1),
|
||||
token: z.string(),
|
||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||
|
||||
signingUrl: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
@ -187,6 +197,8 @@ export const ZGenerateDocumentFromTemplateMutationResponseSchema = z.object({
|
||||
email: z.string().email().min(1),
|
||||
token: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
|
||||
signingUrl: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
@ -229,6 +241,8 @@ export const ZSuccessfulRecipientResponseSchema = z.object({
|
||||
readStatus: z.nativeEnum(ReadStatus),
|
||||
signingStatus: z.nativeEnum(SigningStatus),
|
||||
sendStatus: z.nativeEnum(SendStatus),
|
||||
|
||||
signingUrl: z.string(),
|
||||
});
|
||||
|
||||
export type TSuccessfulRecipientResponseSchema = z.infer<typeof ZSuccessfulRecipientResponseSchema>;
|
||||
@ -279,9 +293,11 @@ export const ZSuccessfulResponseSchema = z.object({
|
||||
|
||||
export type TSuccessfulResponseSchema = z.infer<typeof ZSuccessfulResponseSchema>;
|
||||
|
||||
export const ZSuccessfulSigningResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
export const ZSuccessfulSigningResponseSchema = z
|
||||
.object({
|
||||
message: z.string(),
|
||||
})
|
||||
.and(ZSuccessfulGetDocumentResponseSchema);
|
||||
|
||||
export type TSuccessfulSigningResponseSchema = z.infer<typeof ZSuccessfulSigningResponseSchema>;
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ export type SendDocumentOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
sendEmail?: boolean;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
@ -35,6 +36,7 @@ export const sendDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
sendEmail = true,
|
||||
requestMetadata,
|
||||
}: SendDocumentOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
@ -120,98 +122,102 @@ export const sendDocument = async ({
|
||||
Object.assign(document, result);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
|
||||
return;
|
||||
}
|
||||
if (sendEmail) {
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||
|
||||
const { email, name } = recipient;
|
||||
const selfSigner = email === user.email;
|
||||
const { email, name } = recipient;
|
||||
const selfSigner = email === user.email;
|
||||
|
||||
const selfSignerCustomEmail = `You have initiated the document ${`"${document.title}"`} that requires you to ${RECIPIENT_ROLES_DESCRIPTION[
|
||||
recipient.role
|
||||
].actionVerb.toLowerCase()} it.`;
|
||||
const selfSignerCustomEmail = `You have initiated the document ${`"${document.title}"`} that requires you to ${RECIPIENT_ROLES_DESCRIPTION[
|
||||
recipient.role
|
||||
].actionVerb.toLowerCase()} it.`;
|
||||
|
||||
const customEmailTemplate = {
|
||||
'signer.name': name,
|
||||
'signer.email': email,
|
||||
'document.name': document.title,
|
||||
};
|
||||
const customEmailTemplate = {
|
||||
'signer.name': name,
|
||||
'signer.email': email,
|
||||
'document.name': document.title,
|
||||
};
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
|
||||
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,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail: user.email,
|
||||
assetBaseUrl,
|
||||
signDocumentLink,
|
||||
customBody: renderCustomEmailTemplate(
|
||||
selfSigner && !customEmail?.message ? selfSignerCustomEmail : customEmail?.message || '',
|
||||
customEmailTemplate,
|
||||
),
|
||||
role: recipient.role,
|
||||
selfSigner,
|
||||
});
|
||||
const template = createElement(DocumentInviteEmailTemplate, {
|
||||
documentName: document.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail: user.email,
|
||||
assetBaseUrl,
|
||||
signDocumentLink,
|
||||
customBody: renderCustomEmailTemplate(
|
||||
selfSigner && !customEmail?.message
|
||||
? selfSignerCustomEmail
|
||||
: customEmail?.message || '',
|
||||
customEmailTemplate,
|
||||
),
|
||||
role: recipient.role,
|
||||
selfSigner,
|
||||
});
|
||||
|
||||
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||
|
||||
const emailSubject = selfSigner
|
||||
? `Please ${actionVerb.toLowerCase()} your document`
|
||||
: `Please ${actionVerb.toLowerCase()} this document`;
|
||||
const emailSubject = selfSigner
|
||||
? `Please ${actionVerb.toLowerCase()} your document`
|
||||
: `Please ${actionVerb.toLowerCase()} this document`;
|
||||
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: customEmail?.subject
|
||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||
: emailSubject,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
});
|
||||
|
||||
await tx.recipient.update({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
data: {
|
||||
sendStatus: SendStatus.SENT,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
emailType: recipientEmailType,
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientRole: recipient.role,
|
||||
recipientId: recipient.id,
|
||||
isResending: false,
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
}),
|
||||
);
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: customEmail?.subject
|
||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||
: emailSubject,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
});
|
||||
|
||||
await tx.recipient.update({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
data: {
|
||||
sendStatus: SendStatus.SENT,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
emailType: recipientEmailType,
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientRole: recipient.role,
|
||||
recipientId: recipient.id,
|
||||
isResending: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const allRecipientsHaveNoActionToTake = document.Recipient.every(
|
||||
(recipient) => recipient.role === RecipientRole.CC,
|
||||
|
||||
Reference in New Issue
Block a user