mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
feat: make jobs client type safe
This commit is contained in:
@ -1,6 +1,12 @@
|
|||||||
import { JobClient } from './client/client';
|
import { JobClient } from './client/client';
|
||||||
import { registerJobs } from './definitions';
|
import { SEND_CONFIRMATION_EMAIL_JOB_DEFINITION } from './definitions/send-confirmation-email';
|
||||||
|
import { SEND_SIGNING_EMAIL_JOB_DEFINITION } from './definitions/send-signing-email';
|
||||||
|
|
||||||
export const jobsClient = JobClient.getInstance();
|
/**
|
||||||
|
* The `as const` assertion is load bearing as it provides the correct level of type inference for
|
||||||
registerJobs(jobsClient);
|
* triggering jobs.
|
||||||
|
*/
|
||||||
|
export const jobsClient = new JobClient([
|
||||||
|
SEND_CONFIRMATION_EMAIL_JOB_DEFINITION,
|
||||||
|
SEND_SIGNING_EMAIL_JOB_DEFINITION,
|
||||||
|
] as const);
|
||||||
|
|||||||
@ -2,36 +2,47 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import type { Json } from './json';
|
import type { Json } from './json';
|
||||||
|
|
||||||
export const ZTriggerJobOptionsSchema = z.object({
|
export type SimpleTriggerJobOptions = {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
payload: unknown;
|
||||||
|
timestamp?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ZSimpleTriggerJobOptionsSchema = z.object({
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
payload: z.unknown().refine((x) => x !== undefined, { message: 'payload is required' }),
|
payload: z.unknown().refine((x) => x !== undefined, { message: 'payload is required' }),
|
||||||
timestamp: z.number().optional(),
|
timestamp: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// The Omit is a temporary workaround for a "bug" in the zod library
|
// Map the array to create a union of objects we may accept
|
||||||
// @see: https://github.com/colinhacks/zod/issues/2966
|
export type TriggerJobOptions<Definitions extends Array<JobDefinition> = []> = {
|
||||||
export type TriggerJobOptions = Omit<z.infer<typeof ZTriggerJobOptionsSchema>, 'payload'> & {
|
[K in keyof Definitions]: {
|
||||||
payload: unknown;
|
id?: string;
|
||||||
};
|
name: Definitions[K]['trigger']['name'];
|
||||||
|
payload: Definitions[K]['trigger']['schema'] extends z.ZodType<infer Shape> ? Shape : unknown;
|
||||||
|
timestamp?: number;
|
||||||
|
};
|
||||||
|
}[number];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export type JobDefinition<T = any> = {
|
export type JobDefinition<Name extends string = string, Schema = any> = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
trigger: {
|
trigger: {
|
||||||
name: string;
|
name: Name;
|
||||||
schema?: z.ZodSchema<T>;
|
schema?: z.ZodType<Schema>;
|
||||||
};
|
};
|
||||||
handler: (options: { payload: T; io: JobRunIO }) => Promise<Json | void>;
|
handler: (options: { payload: Schema; io: JobRunIO }) => Promise<Json | void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface JobRunIO {
|
export interface JobRunIO {
|
||||||
// stableRun<T extends Json | void>(cacheKey: string, callback: (io: JobRunIO) => T | Promise<T>): Promise<T>;
|
// stableRun<T extends Json | void>(cacheKey: string, callback: (io: JobRunIO) => T | Promise<T>): Promise<T>;
|
||||||
runTask<T extends Json | void>(cacheKey: string, callback: () => Promise<T>): Promise<T>;
|
runTask<T extends Json | void>(cacheKey: string, callback: () => Promise<T>): Promise<T>;
|
||||||
triggerJob(cacheKey: string, options: TriggerJobOptions): Promise<unknown>;
|
triggerJob(cacheKey: string, options: SimpleTriggerJobOptions): Promise<unknown>;
|
||||||
wait(cacheKey: string, ms: number): Promise<void>;
|
wait(cacheKey: string, ms: number): Promise<void>;
|
||||||
logger: {
|
logger: {
|
||||||
info(...args: unknown[]): void;
|
info(...args: unknown[]): void;
|
||||||
@ -41,3 +52,7 @@ export interface JobRunIO {
|
|||||||
log(...args: unknown[]): void;
|
log(...args: unknown[]): void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defineJob = <N extends string, T = unknown>(
|
||||||
|
job: JobDefinition<N, T>,
|
||||||
|
): JobDefinition<N, T> => job;
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
import type { JobDefinition, TriggerJobOptions } from './_internal/job';
|
import type { JobDefinition, SimpleTriggerJobOptions } from './_internal/job';
|
||||||
|
|
||||||
export abstract class BaseJobProvider {
|
export abstract class BaseJobProvider {
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
public async triggerJob(_options: TriggerJobOptions): Promise<void> {
|
public async triggerJob(_options: SimpleTriggerJobOptions): Promise<void> {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
public defineJob<T>(_job: JobDefinition<T>): void {
|
public defineJob<N extends string, T>(_job: JobDefinition<N, T>): void {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,12 +3,12 @@ import type { BaseJobProvider as JobClientProvider } from './base';
|
|||||||
import { LocalJobProvider } from './local';
|
import { LocalJobProvider } from './local';
|
||||||
import { TriggerJobProvider } from './trigger';
|
import { TriggerJobProvider } from './trigger';
|
||||||
|
|
||||||
export class JobClient {
|
export class JobClient<T extends Array<JobDefinition> = []> {
|
||||||
private static _instance: JobClient;
|
private static _instance: JobClient;
|
||||||
|
|
||||||
private _provider: JobClientProvider;
|
private _provider: JobClientProvider;
|
||||||
|
|
||||||
private constructor() {
|
public constructor(definitions: T) {
|
||||||
if (process.env.NEXT_PRIVATE_JOBS_PROVIDER === 'trigger') {
|
if (process.env.NEXT_PRIVATE_JOBS_PROVIDER === 'trigger') {
|
||||||
this._provider = TriggerJobProvider.getInstance();
|
this._provider = TriggerJobProvider.getInstance();
|
||||||
|
|
||||||
@ -16,23 +16,27 @@ export class JobClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._provider = LocalJobProvider.getInstance();
|
this._provider = LocalJobProvider.getInstance();
|
||||||
|
|
||||||
|
definitions.forEach((definition) => {
|
||||||
|
this._provider.defineJob(definition);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getInstance() {
|
// public static getInstance() {
|
||||||
if (!this._instance) {
|
// if (!this._instance) {
|
||||||
this._instance = new JobClient();
|
// this._instance = new JobClient();
|
||||||
}
|
// }
|
||||||
|
|
||||||
return this._instance;
|
// return this._instance;
|
||||||
}
|
// }
|
||||||
|
|
||||||
public async triggerJob(options: TriggerJobOptions) {
|
public async triggerJob(options: TriggerJobOptions<T>) {
|
||||||
return this._provider.triggerJob(options);
|
return this._provider.triggerJob(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public defineJob<T>(job: JobDefinition<T>) {
|
// public defineJob<N extends string, T>(job: JobDefinition<N, T>) {
|
||||||
return this._provider.defineJob(job);
|
// return this._provider.defineJob(job);
|
||||||
}
|
// }
|
||||||
|
|
||||||
public getApiHandler() {
|
public getApiHandler() {
|
||||||
return this._provider.getApiHandler();
|
return this._provider.getApiHandler();
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import { verify } from '../../server-only/crypto/verify';
|
|||||||
import {
|
import {
|
||||||
type JobDefinition,
|
type JobDefinition,
|
||||||
type JobRunIO,
|
type JobRunIO,
|
||||||
type TriggerJobOptions,
|
type SimpleTriggerJobOptions,
|
||||||
ZTriggerJobOptionsSchema,
|
ZSimpleTriggerJobOptionsSchema,
|
||||||
} from './_internal/job';
|
} from './_internal/job';
|
||||||
import type { Json } from './_internal/json';
|
import type { Json } from './_internal/json';
|
||||||
import { BaseJobProvider } from './base';
|
import { BaseJobProvider } from './base';
|
||||||
@ -35,14 +35,14 @@ export class LocalJobProvider extends BaseJobProvider {
|
|||||||
return this._instance;
|
return this._instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public defineJob<T>(definition: JobDefinition<T>) {
|
public defineJob<N extends string, T>(definition: JobDefinition<N, T>) {
|
||||||
this._jobDefinitions[definition.id] = {
|
this._jobDefinitions[definition.id] = {
|
||||||
...definition,
|
...definition,
|
||||||
enabled: definition.enabled ?? true,
|
enabled: definition.enabled ?? true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async triggerJob(options: TriggerJobOptions) {
|
public async triggerJob(options: SimpleTriggerJobOptions) {
|
||||||
console.log({ jobDefinitions: this._jobDefinitions });
|
console.log({ jobDefinitions: this._jobDefinitions });
|
||||||
|
|
||||||
const eligibleJobs = Object.values(this._jobDefinitions).filter(
|
const eligibleJobs = Object.values(this._jobDefinitions).filter(
|
||||||
@ -87,9 +87,9 @@ export class LocalJobProvider extends BaseJobProvider {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
const options = await json(req)
|
const options = await json(req)
|
||||||
.then(async (data) => ZTriggerJobOptionsSchema.parseAsync(data))
|
.then(async (data) => ZSimpleTriggerJobOptionsSchema.parseAsync(data))
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
.then((data) => data as TriggerJobOptions)
|
.then((data) => data as SimpleTriggerJobOptions)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
|
|
||||||
if (!options) {
|
if (!options) {
|
||||||
@ -224,7 +224,7 @@ export class LocalJobProvider extends BaseJobProvider {
|
|||||||
private async submitJobToEndpoint(options: {
|
private async submitJobToEndpoint(options: {
|
||||||
jobId: string;
|
jobId: string;
|
||||||
jobDefinitionId: string;
|
jobDefinitionId: string;
|
||||||
data: TriggerJobOptions;
|
data: SimpleTriggerJobOptions;
|
||||||
isRetry?: boolean;
|
isRetry?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { jobId, jobDefinitionId, data, isRetry } = options;
|
const { jobId, jobDefinitionId, data, isRetry } = options;
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { createPagesRoute } from '@trigger.dev/nextjs';
|
|||||||
import type { IO } from '@trigger.dev/sdk';
|
import type { IO } from '@trigger.dev/sdk';
|
||||||
import { TriggerClient, eventTrigger } from '@trigger.dev/sdk';
|
import { TriggerClient, eventTrigger } from '@trigger.dev/sdk';
|
||||||
|
|
||||||
import type { JobDefinition, JobRunIO, TriggerJobOptions } from './_internal/job';
|
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
|
||||||
import { BaseJobProvider } from './base';
|
import { BaseJobProvider } from './base';
|
||||||
|
|
||||||
export class TriggerJobProvider extends BaseJobProvider {
|
export class TriggerJobProvider extends BaseJobProvider {
|
||||||
@ -32,7 +32,7 @@ export class TriggerJobProvider extends BaseJobProvider {
|
|||||||
return this._instance;
|
return this._instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public defineJob<T>(job: JobDefinition<T>): void {
|
public defineJob<N extends string, T>(job: JobDefinition<N, T>): void {
|
||||||
this._client.defineJob({
|
this._client.defineJob({
|
||||||
id: job.id,
|
id: job.id,
|
||||||
name: job.name,
|
name: job.name,
|
||||||
@ -45,12 +45,12 @@ export class TriggerJobProvider extends BaseJobProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async triggerJob(_options: TriggerJobOptions): Promise<void> {
|
public async triggerJob(options: SimpleTriggerJobOptions): Promise<void> {
|
||||||
await this._client.sendEvent({
|
await this._client.sendEvent({
|
||||||
id: _options.id,
|
id: options.id,
|
||||||
name: _options.name,
|
name: options.name,
|
||||||
payload: _options.payload,
|
payload: options.payload,
|
||||||
timestamp: _options.timestamp ? new Date(_options.timestamp) : undefined,
|
timestamp: options.timestamp ? new Date(options.timestamp) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
import type { JobClient } from '../client/client';
|
|
||||||
import { registerSendConfirmationEmailJob } from './send-confirmation-email';
|
|
||||||
|
|
||||||
export const registerJobs = (client: JobClient) => {
|
|
||||||
registerSendConfirmationEmailJob(client);
|
|
||||||
};
|
|
||||||
@ -1,25 +1,23 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { sendConfirmationToken } from '../../server-only/user/send-confirmation-token';
|
import { sendConfirmationToken } from '../../server-only/user/send-confirmation-token';
|
||||||
import type { JobClient } from '../client/client';
|
import type { JobDefinition } from '../client/_internal/job';
|
||||||
|
|
||||||
export const registerSendConfirmationEmailJob = (client: JobClient) => {
|
export const SEND_CONFIRMATION_EMAIL_JOB_DEFINITION = {
|
||||||
client.defineJob({
|
id: 'send.confirmation.email',
|
||||||
id: 'send.confirmation.email',
|
name: 'Send Confirmation Email',
|
||||||
name: 'Send Confirmation Email',
|
version: '1.0.0',
|
||||||
version: '1.0.0',
|
trigger: {
|
||||||
trigger: {
|
name: 'send.confirmation.email',
|
||||||
name: 'send.confirmation.email',
|
schema: z.object({
|
||||||
schema: z.object({
|
email: z.string().email(),
|
||||||
email: z.string().email(),
|
force: z.boolean().optional(),
|
||||||
force: z.boolean().optional(),
|
}),
|
||||||
}),
|
},
|
||||||
},
|
handler: async ({ payload }) => {
|
||||||
handler: async ({ payload }) => {
|
await sendConfirmationToken({
|
||||||
await sendConfirmationToken({
|
email: payload.email,
|
||||||
email: payload.email,
|
force: payload.force,
|
||||||
force: payload.force,
|
});
|
||||||
});
|
},
|
||||||
},
|
} as const satisfies JobDefinition;
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
149
packages/lib/jobs/definitions/send-signing-email.ts
Normal file
149
packages/lib/jobs/definitions/send-signing-email.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { mailer } from '@documenso/email/mailer';
|
||||||
|
import { render } from '@documenso/email/render';
|
||||||
|
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
|
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||||
|
import {
|
||||||
|
RECIPIENT_ROLES_DESCRIPTION,
|
||||||
|
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||||
|
} from '../../constants/recipient-roles';
|
||||||
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||||
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
|
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
||||||
|
import type { JobDefinition } from '../client/_internal/job';
|
||||||
|
import { ZRequestMetadataSchema } from '../../universal/extract-request-metadata';
|
||||||
|
|
||||||
|
export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
|
||||||
|
id: 'send.signing.email',
|
||||||
|
name: 'Send Signing Email',
|
||||||
|
version: '1.0.0',
|
||||||
|
trigger: {
|
||||||
|
name: 'send.signing.email',
|
||||||
|
schema: z.object({
|
||||||
|
userId: z.number(),
|
||||||
|
documentId: z.number(),
|
||||||
|
recipientId: z.number(),
|
||||||
|
requestMetadata: ZRequestMetadataSchema.optional(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
handler: async ({ payload, io }) => {
|
||||||
|
const { userId, documentId, recipientId, requestMetadata } = payload;
|
||||||
|
|
||||||
|
const [user, document, recipient] = await Promise.all([
|
||||||
|
prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
status: DocumentStatus.PENDING,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
documentMeta: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.recipient.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: recipientId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { documentMeta } = document;
|
||||||
|
|
||||||
|
if (recipient.role === RecipientRole.CC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||||
|
const roleDescription = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||||
|
|
||||||
|
const isSendingToSelf = recipient.email === user.email;
|
||||||
|
const selfSignerMessage = `You have initiated the document ${`"${document.title}"`} that requires you to ${roleDescription.actionVerb.toLowerCase()} it.`;
|
||||||
|
|
||||||
|
const emailSubject = isSendingToSelf
|
||||||
|
? `Please ${roleDescription.actionVerb.toLowerCase()} your document`
|
||||||
|
: `Please ${roleDescription.actionVerb.toLowerCase()} this document`;
|
||||||
|
|
||||||
|
const emailTemplateVariables = {
|
||||||
|
'signer.name': recipient.name,
|
||||||
|
'signer.email': recipient.email,
|
||||||
|
'document.title': document.title,
|
||||||
|
};
|
||||||
|
|
||||||
|
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||||
|
const signDocumentUrl = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
|
||||||
|
|
||||||
|
const template = createElement(DocumentInviteEmailTemplate, {
|
||||||
|
documentName: document.title,
|
||||||
|
inviterName: user.name || undefined,
|
||||||
|
inviterEmail: user.email,
|
||||||
|
assetBaseUrl,
|
||||||
|
signDocumentLink: signDocumentUrl,
|
||||||
|
customBody: renderCustomEmailTemplate(
|
||||||
|
isSendingToSelf ? selfSignerMessage : documentMeta?.message || '',
|
||||||
|
emailTemplateVariables,
|
||||||
|
),
|
||||||
|
role: recipient.role,
|
||||||
|
selfSigner: isSendingToSelf,
|
||||||
|
});
|
||||||
|
|
||||||
|
await io.runTask('send-signing-email', async () => {
|
||||||
|
await mailer.sendMail({
|
||||||
|
to: {
|
||||||
|
name: recipient.name,
|
||||||
|
address: recipient.email,
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
name: FROM_NAME,
|
||||||
|
address: FROM_ADDRESS,
|
||||||
|
},
|
||||||
|
subject: renderCustomEmailTemplate(
|
||||||
|
documentMeta?.subject || emailSubject,
|
||||||
|
emailTemplateVariables,
|
||||||
|
),
|
||||||
|
html: render(template),
|
||||||
|
text: render(template, { plainText: true }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await io.runTask('update-recipient', async () => {
|
||||||
|
await prisma.recipient.update({
|
||||||
|
where: {
|
||||||
|
id: recipient.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
sendStatus: SendStatus.SENT,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await io.runTask('store-audit-log', async () => {
|
||||||
|
await prisma.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||||
|
documentId: document.id,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
emailType,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
recipientName: recipient.name,
|
||||||
|
recipientEmail: recipient.email,
|
||||||
|
recipientRole: recipient.role,
|
||||||
|
isResending: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
} as const satisfies JobDefinition;
|
||||||
@ -1,16 +1,9 @@
|
|||||||
import { createElement } from 'react';
|
|
||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
|
||||||
import { render } from '@documenso/email/render';
|
|
||||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
|
||||||
import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
|
import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
|
||||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import {
|
import {
|
||||||
DocumentSource,
|
DocumentSource,
|
||||||
@ -21,11 +14,7 @@ import {
|
|||||||
} from '@documenso/prisma/client';
|
} from '@documenso/prisma/client';
|
||||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { jobsClient } from '../../jobs/client';
|
||||||
import {
|
|
||||||
RECIPIENT_ROLES_DESCRIPTION,
|
|
||||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
|
||||||
} from '../../constants/recipient-roles';
|
|
||||||
import { getFile } from '../../universal/upload/get-file';
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||||
@ -137,92 +126,15 @@ export const sendDocument = async ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
await jobsClient.triggerJob({
|
||||||
|
name: 'send.signing.email',
|
||||||
const { email, name } = recipient;
|
payload: {
|
||||||
const selfSigner = email === user.email;
|
userId,
|
||||||
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
documentId,
|
||||||
const recipientActionVerb = actionVerb.toLowerCase();
|
recipientId: recipient.id,
|
||||||
|
requestMetadata,
|
||||||
let emailMessage = customEmail?.message || '';
|
|
||||||
let emailSubject = `Please ${recipientActionVerb} this document`;
|
|
||||||
|
|
||||||
if (selfSigner) {
|
|
||||||
emailMessage = `You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`;
|
|
||||||
emailSubject = `Please ${recipientActionVerb} your document`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDirectTemplate) {
|
|
||||||
emailMessage = `A document was created by your direct template that requires you to ${recipientActionVerb} it.`;
|
|
||||||
emailSubject = `Please ${recipientActionVerb} this document created by your direct template`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 template = createElement(DocumentInviteEmailTemplate, {
|
|
||||||
documentName: document.title,
|
|
||||||
inviterName: user.name || undefined,
|
|
||||||
inviterEmail: user.email,
|
|
||||||
assetBaseUrl,
|
|
||||||
signDocumentLink,
|
|
||||||
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
|
|
||||||
role: recipient.role,
|
|
||||||
selfSigner,
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
{ timeout: 30_000 },
|
});
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,10 +5,12 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
const ZIpSchema = z.string().ip();
|
const ZIpSchema = z.string().ip();
|
||||||
|
|
||||||
export type RequestMetadata = {
|
export const ZRequestMetadataSchema = z.object({
|
||||||
ipAddress?: string;
|
ipAddress: ZIpSchema.optional(),
|
||||||
userAgent?: string;
|
userAgent: z.string().optional(),
|
||||||
};
|
});
|
||||||
|
|
||||||
|
export type RequestMetadata = z.infer<typeof ZRequestMetadataSchema>;
|
||||||
|
|
||||||
export const extractNextApiRequestMetadata = (req: NextApiRequest): RequestMetadata => {
|
export const extractNextApiRequestMetadata = (req: NextApiRequest): RequestMetadata => {
|
||||||
const parsedIp = ZIpSchema.safeParse(req.headers['x-forwarded-for'] || req.socket.remoteAddress);
|
const parsedIp = ZIpSchema.safeParse(req.headers['x-forwarded-for'] || req.socket.remoteAddress);
|
||||||
|
|||||||
Reference in New Issue
Block a user