fix: add email reporting (#2918)

This commit is contained in:
David Nguyen
2026-06-03 16:05:39 +10:00
committed by GitHub
parent 743d31651f
commit 993a494784
24 changed files with 521 additions and 33 deletions
@@ -211,6 +211,8 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai
};
const downloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}/complete`;
const reportUrl =
recipient.role === RecipientRole.CC ? `${NEXT_PUBLIC_WEBAPP_URL()}/report/${recipient.token}` : undefined;
const template = createElement(DocumentCompletedEmailTemplate, {
documentName: envelope.title,
@@ -220,6 +222,7 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai
isDirectTemplate && envelope.documentMeta?.message
? renderCustomEmailTemplate(envelope.documentMeta.message, customEmailTemplate)
: undefined,
reportUrl,
});
const [html, text] = await Promise.all([
@@ -165,6 +165,7 @@ export const run = async ({ payload, io }: { payload: TSendSigningEmailJobDefini
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
const reportUrl = `${NEXT_PUBLIC_WEBAPP_URL()}/report/${recipient.token}`;
const template = createElement(DocumentInviteEmailTemplate, {
documentName: envelope.title,
@@ -180,6 +181,7 @@ export const run = async ({ payload, io }: { payload: TSendSigningEmailJobDefini
teamName: team?.name,
teamEmail: team?.teamEmail?.email,
includeSenderDetails: settings.includeSenderDetails,
reportUrl,
});
if (isRecipientEmailValidForSending(recipient)) {
@@ -154,6 +154,7 @@ export const run = async ({ payload, io }: { payload: TProcessSigningReminderJob
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
const reportUrl = `${NEXT_PUBLIC_WEBAPP_URL()}/report/${recipient.token}`;
// Meter reminder emails against the organisation email quota/stats. Reminders
// are unsolicited (the recipient didn't opt in to them) and can recur, so they
@@ -188,6 +189,7 @@ export const run = async ({ payload, io }: { payload: TProcessSigningReminderJob
signDocumentLink,
customBody: emailMessage,
role: recipient.role,
reportUrl,
});
const [html, text] = await Promise.all([
@@ -1,5 +1,6 @@
import { prisma } from '@documenso/prisma';
import { EnvelopeType, type Prisma } from '@prisma/client';
import { z } from 'zod';
import type { FindResultResponse } from '../../types/search-params';
@@ -9,6 +10,16 @@ export interface AdminFindDocumentsOptions {
perPage?: number;
}
const ZPositiveIntegerSchema = z.coerce.number().int().positive();
const emptyResponse = {
data: [],
count: 0,
currentPage: 1,
perPage: 10,
totalPages: 0,
};
export const adminFindDocuments = async ({ query, page = 1, perPage = 10 }: AdminFindDocumentsOptions) => {
let termFilters: Prisma.EnvelopeWhereInput | undefined = !query
? undefined
@@ -19,7 +30,35 @@ export const adminFindDocuments = async ({ query, page = 1, perPage = 10 }: Admi
},
};
if (query && query.startsWith('envelope_')) {
if (query?.startsWith('user:')) {
const parsedUserId = ZPositiveIntegerSchema.safeParse(query.slice('user:'.length));
if (parsedUserId.success) {
termFilters = {
userId: {
equals: parsedUserId.data,
},
};
} else {
return emptyResponse;
}
}
if (query?.startsWith('team:')) {
const parsedTeamId = ZPositiveIntegerSchema.safeParse(query.slice('team:'.length));
if (parsedTeamId.success) {
termFilters = {
teamId: {
equals: parsedTeamId.data,
},
};
} else {
return emptyResponse;
}
}
if (query && query?.startsWith('envelope_')) {
termFilters = {
id: {
equals: query,
@@ -27,7 +66,7 @@ export const adminFindDocuments = async ({ query, page = 1, perPage = 10 }: Admi
};
}
if (query && query.startsWith('document_')) {
if (query && query?.startsWith('document_')) {
termFilters = {
secondaryId: {
equals: query,
@@ -210,6 +210,7 @@ export const resendDocument = async ({ id, userId, recipients, teamId, requestMe
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
const reportUrl = `${NEXT_PUBLIC_WEBAPP_URL()}/report/${recipient.token}`;
const template = createElement(DocumentInviteEmailTemplate, {
documentName: envelope.title,
@@ -225,6 +226,7 @@ export const resendDocument = async ({ id, userId, recipients, teamId, requestMe
selfSigner,
organisationType,
teamName: envelope.team?.name,
reportUrl,
});
const [html, text] = await Promise.all([
@@ -66,6 +66,12 @@ export const linkOrgAccountRateLimit = createRateLimit({
window: '1h',
});
export const reportSenderRateLimit = createRateLimit({
action: 'recipient.report-sender',
max: 1,
window: '7d',
});
// ---- API (Tier 4 - Standard) ----
export const apiV1RateLimit = createRateLimit({