Files
documenso/packages/trpc/server/document-router/access-auth-request-2fa-email.ts
T
Lucas Smith 844af17ec2 feat: move email sending into background jobs for retry support
Each direct mailer.sendMail() call is replaced by a dedicated background
job so that email delivery failures can be retried independently.

New jobs: send-pending-email, send-completed-email, send-forgot-password-email,
send-document-super-delete-email, send-recipient-removed-email,
send-document-deleted-emails, send-2fa-token-email, send-resend-document-email,
send-direct-template-created-email.

Existing handlers (send-document-cancelled-emails, send-organisation-member-*,
send-recipient-signed-email) have io.runTask wrappers removed since they
interfere with the job scheduler. Job triggers are dispatched after
transactions commit to avoid race conditions with uncommitted data.
2026-02-20 23:25:37 +11:00

100 lines
2.9 KiB
TypeScript

import { EnvelopeType } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { DateTime } from 'luxon';
import { jobs } from '@documenso/lib/jobs/client';
import { TWO_FACTOR_EMAIL_EXPIRATION_MINUTES } from '@documenso/lib/server-only/2fa/email/constants';
import { assertRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limit-middleware';
import { request2FAEmailRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limits';
import { DocumentAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { procedure } from '../trpc';
import {
ZAccessAuthRequest2FAEmailRequestSchema,
ZAccessAuthRequest2FAEmailResponseSchema,
} from './access-auth-request-2fa-email.types';
export const accessAuthRequest2FAEmailRoute = procedure
.input(ZAccessAuthRequest2FAEmailRequestSchema)
.output(ZAccessAuthRequest2FAEmailResponseSchema)
.mutation(async ({ input, ctx }) => {
try {
const { token } = input;
const rateLimitResult = await request2FAEmailRateLimit.check({
ip: ctx.metadata.requestMetadata.ipAddress ?? 'unknown',
identifier: token,
});
assertRateLimit(rateLimitResult);
// Get document and recipient by token
const envelope = await prisma.envelope.findFirst({
where: {
type: EnvelopeType.DOCUMENT,
recipients: {
some: {
token,
},
},
},
include: {
recipients: {
where: {
token,
},
},
},
});
if (!envelope) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Document not found',
});
}
const [recipient] = envelope.recipients;
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: envelope.authOptions,
recipientAuth: recipient.authOptions,
});
if (!derivedRecipientAccessAuth.includes(DocumentAuth.TWO_FACTOR_AUTH)) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: '2FA is not required for this document',
});
}
const expiresAt = DateTime.now().plus({ minutes: TWO_FACTOR_EMAIL_EXPIRATION_MINUTES });
await jobs.triggerJob({
name: 'send.2fa.token.email',
payload: {
envelopeId: envelope.id,
recipientId: recipient.id,
},
});
return {
success: true,
expiresAt: expiresAt.toJSDate(),
};
} catch (error) {
console.error('Error sending access auth 2FA email:', error);
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to send 2FA email',
});
}
});