mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 01:01:49 +10:00
feat: migrate nextjs to rr7
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import { JobClient } from './client/client';
|
||||
import { SEND_CONFIRMATION_EMAIL_JOB_DEFINITION } from './definitions/emails/send-confirmation-email';
|
||||
import { SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION } from './definitions/emails/send-password-reset-success-email';
|
||||
import { SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-recipient-signed-email';
|
||||
import { SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION } from './definitions/emails/send-rejection-emails';
|
||||
import { SEND_SIGNING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-signing-email';
|
||||
@ -19,6 +20,7 @@ export const jobsClient = new JobClient([
|
||||
SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION,
|
||||
SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION,
|
||||
SEAL_DOCUMENT_JOB_DEFINITION,
|
||||
SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION,
|
||||
SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION,
|
||||
SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION,
|
||||
] as const);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type { Context as HonoContext } from 'hono';
|
||||
|
||||
import type { JobDefinition, SimpleTriggerJobOptions } from './_internal/job';
|
||||
|
||||
@ -13,7 +13,7 @@ export abstract class BaseJobProvider {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public getApiHandler(): (req: NextApiRequest, res: NextApiResponse) => Promise<Response | void> {
|
||||
public getApiHandler(): (req: HonoContext) => Promise<Response | void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,17 @@
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { env } from '../../utils/env';
|
||||
import type { JobDefinition, TriggerJobOptions } from './_internal/job';
|
||||
import type { BaseJobProvider as JobClientProvider } from './base';
|
||||
import { InngestJobProvider } from './inngest';
|
||||
import { LocalJobProvider } from './local';
|
||||
import { TriggerJobProvider } from './trigger';
|
||||
|
||||
export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
|
||||
private _provider: JobClientProvider;
|
||||
|
||||
public constructor(definitions: T) {
|
||||
this._provider = match(process.env.NEXT_PRIVATE_JOBS_PROVIDER)
|
||||
this._provider = match(env('NEXT_PRIVATE_JOBS_PROVIDER'))
|
||||
.with('inngest', () => InngestJobProvider.getInstance())
|
||||
.with('trigger', () => TriggerJobProvider.getInstance())
|
||||
.otherwise(() => LocalJobProvider.getInstance());
|
||||
|
||||
definitions.forEach((definition) => {
|
||||
|
||||
3
packages/lib/jobs/client/index.ts
Normal file
3
packages/lib/jobs/client/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Empty file for build reasons.
|
||||
// Vite build seems to assume jobs/client.ts = jobs/client/index.ts and therefore will throw an error that the file is missing.
|
||||
// Could refactor the files, but this is easier.
|
||||
@ -1,12 +1,10 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import type { Context as HonoContext } from 'hono';
|
||||
import type { Context, Handler, InngestFunction } from 'inngest';
|
||||
import { Inngest as InngestClient } from 'inngest';
|
||||
import { serve as createHonoPagesRoute } from 'inngest/hono';
|
||||
import type { Logger } from 'inngest/middleware/logger';
|
||||
import { serve as createPagesRoute } from 'inngest/next';
|
||||
import { json } from 'micro';
|
||||
|
||||
import { env } from '../../utils/env';
|
||||
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
|
||||
import { BaseJobProvider } from './base';
|
||||
|
||||
@ -26,8 +24,8 @@ export class InngestJobProvider extends BaseJobProvider {
|
||||
static getInstance() {
|
||||
if (!this._instance) {
|
||||
const client = new InngestClient({
|
||||
id: process.env.NEXT_PRIVATE_INNGEST_APP_ID || 'documenso-app',
|
||||
eventKey: process.env.INNGEST_EVENT_KEY || process.env.NEXT_PRIVATE_INNGEST_EVENT_KEY,
|
||||
id: env('NEXT_PRIVATE_INNGEST_APP_ID') || 'documenso-app',
|
||||
eventKey: env('INNGEST_EVENT_KEY') || env('NEXT_PRIVATE_INNGEST_EVENT_KEY'),
|
||||
});
|
||||
|
||||
this._instance = new InngestJobProvider({ client });
|
||||
@ -73,24 +71,36 @@ export class InngestJobProvider extends BaseJobProvider {
|
||||
});
|
||||
}
|
||||
|
||||
// public getApiHandler() {
|
||||
// const handler = createPagesRoute({
|
||||
// client: this._client,
|
||||
// functions: this._functions,
|
||||
// });
|
||||
|
||||
// return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
// // Since body-parser is disabled for this route we need to patch in the parsed body
|
||||
// if (req.headers['content-type'] === 'application/json') {
|
||||
// Object.assign(req, {
|
||||
// body: await json(req),
|
||||
// });
|
||||
// }
|
||||
|
||||
// // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
// const nextReq = req as unknown as NextRequest;
|
||||
|
||||
// return await handler(nextReq, res);
|
||||
// };
|
||||
// }
|
||||
|
||||
// Todo: Do we need to handle the above?
|
||||
public getApiHandler() {
|
||||
const handler = createPagesRoute({
|
||||
client: this._client,
|
||||
functions: this._functions,
|
||||
});
|
||||
return async (context: HonoContext) => {
|
||||
const handler = createHonoPagesRoute({
|
||||
client: this._client,
|
||||
functions: this._functions,
|
||||
});
|
||||
|
||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
// Since body-parser is disabled for this route we need to patch in the parsed body
|
||||
if (req.headers['content-type'] === 'application/json') {
|
||||
Object.assign(req, {
|
||||
body: await json(req),
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const nextReq = req as unknown as NextRequest;
|
||||
|
||||
return await handler(nextReq, res);
|
||||
return await handler(context);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
import { json } from 'micro';
|
||||
import { BackgroundJobStatus, Prisma } from '@prisma/client';
|
||||
import type { Context as HonoContext } from 'hono';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { BackgroundJobStatus, Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||
import { sign } from '../../server-only/crypto/sign';
|
||||
@ -70,26 +68,27 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
);
|
||||
}
|
||||
|
||||
public getApiHandler() {
|
||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
public getApiHandler(): (c: HonoContext) => Promise<Response | void> {
|
||||
return async (c: HonoContext) => {
|
||||
const req = c.req;
|
||||
|
||||
if (req.method !== 'POST') {
|
||||
res.status(405).send('Method not allowed');
|
||||
return c.text('Method not allowed', 405);
|
||||
}
|
||||
|
||||
const jobId = req.headers['x-job-id'];
|
||||
const signature = req.headers['x-job-signature'];
|
||||
const isRetry = req.headers['x-job-retry'] !== undefined;
|
||||
const jobId = req.header('x-job-id');
|
||||
const signature = req.header('x-job-signature');
|
||||
const isRetry = req.header('x-job-retry') !== undefined;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const options = await json(req)
|
||||
const options = await req
|
||||
.json()
|
||||
.then(async (data) => ZSimpleTriggerJobOptionsSchema.parseAsync(data))
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
.then((data) => data as SimpleTriggerJobOptions)
|
||||
.catch(() => null);
|
||||
|
||||
if (!options) {
|
||||
res.status(400).send('Bad request');
|
||||
return;
|
||||
return c.text('Bad request', 400);
|
||||
}
|
||||
|
||||
const definition = this._jobDefinitions[options.name];
|
||||
@ -99,33 +98,28 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
typeof signature !== 'string' ||
|
||||
typeof options !== 'object'
|
||||
) {
|
||||
res.status(400).send('Bad request');
|
||||
return;
|
||||
return c.text('Bad request', 400);
|
||||
}
|
||||
|
||||
if (!definition) {
|
||||
res.status(404).send('Job not found');
|
||||
return;
|
||||
return c.text('Job not found', 404);
|
||||
}
|
||||
|
||||
if (definition && !definition.enabled) {
|
||||
console.log('Attempted to trigger a disabled job', options.name);
|
||||
|
||||
res.status(404).send('Job not found');
|
||||
return;
|
||||
return c.text('Job not found', 404);
|
||||
}
|
||||
|
||||
if (!signature || !verify(options, signature)) {
|
||||
res.status(401).send('Unauthorized');
|
||||
return;
|
||||
return c.text('Unauthorized', 401);
|
||||
}
|
||||
|
||||
if (definition.trigger.schema) {
|
||||
const result = definition.trigger.schema.safeParse(options.payload);
|
||||
|
||||
if (!result.success) {
|
||||
res.status(400).send('Bad request');
|
||||
return;
|
||||
return c.text('Bad request', 400);
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,8 +142,7 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
.catch(() => null);
|
||||
|
||||
if (!backgroundJob) {
|
||||
res.status(404).send('Job not found');
|
||||
return;
|
||||
return c.text('Job not found', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -188,8 +181,7 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
},
|
||||
});
|
||||
|
||||
res.status(500).send('Task exceeded retries');
|
||||
return;
|
||||
return c.text('Task exceeded retries', 500);
|
||||
}
|
||||
|
||||
backgroundJob = await prisma.backgroundJob.update({
|
||||
@ -209,7 +201,7 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send('OK');
|
||||
return c.text('OK', 200);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
import { createPagesRoute } from '@trigger.dev/nextjs';
|
||||
import type { IO } from '@trigger.dev/sdk';
|
||||
import { TriggerClient, eventTrigger } from '@trigger.dev/sdk';
|
||||
|
||||
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
|
||||
import { BaseJobProvider } from './base';
|
||||
|
||||
export class TriggerJobProvider extends BaseJobProvider {
|
||||
private static _instance: TriggerJobProvider;
|
||||
|
||||
private _client: TriggerClient;
|
||||
|
||||
private constructor(options: { client: TriggerClient }) {
|
||||
super();
|
||||
|
||||
this._client = options.client;
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!this._instance) {
|
||||
const client = new TriggerClient({
|
||||
id: 'documenso-app',
|
||||
apiKey: process.env.NEXT_PRIVATE_TRIGGER_API_KEY,
|
||||
apiUrl: process.env.NEXT_PRIVATE_TRIGGER_API_URL,
|
||||
});
|
||||
|
||||
this._instance = new TriggerJobProvider({ client });
|
||||
}
|
||||
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
public defineJob<N extends string, T>(job: JobDefinition<N, T>): void {
|
||||
this._client.defineJob({
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
version: job.version,
|
||||
trigger: eventTrigger({
|
||||
name: job.trigger.name,
|
||||
schema: job.trigger.schema,
|
||||
}),
|
||||
run: async (payload, io) => job.handler({ payload, io: this.convertTriggerIoToJobRunIo(io) }),
|
||||
});
|
||||
}
|
||||
|
||||
public async triggerJob(options: SimpleTriggerJobOptions): Promise<void> {
|
||||
await this._client.sendEvent({
|
||||
id: options.id,
|
||||
name: options.name,
|
||||
payload: options.payload,
|
||||
timestamp: options.timestamp ? new Date(options.timestamp) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
public getApiHandler() {
|
||||
const { handler } = createPagesRoute(this._client);
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
private convertTriggerIoToJobRunIo(io: IO) {
|
||||
return {
|
||||
wait: io.wait,
|
||||
logger: io.logger,
|
||||
runTask: async (cacheKey, callback) => io.runTask(cacheKey, callback),
|
||||
triggerJob: async (cacheKey, payload) =>
|
||||
io.sendEvent(cacheKey, {
|
||||
...payload,
|
||||
timestamp: payload.timestamp ? new Date(payload.timestamp) : undefined,
|
||||
}),
|
||||
} satisfies JobRunIO;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { sendResetPassword } from '../../../server-only/auth/send-reset-password';
|
||||
import type { TSendPasswordResetSuccessEmailJobDefinition } from './send-password-reset-success-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
}: {
|
||||
payload: TSendPasswordResetSuccessEmailJobDefinition;
|
||||
}) => {
|
||||
await sendResetPassword({
|
||||
userId: payload.userId,
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_ID = 'send.password.reset.success.email';
|
||||
|
||||
const SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
userId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendPasswordResetSuccessEmailJobDefinition = z.infer<
|
||||
typeof SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Password Reset Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload }) => {
|
||||
const handler = await import('./send-password-reset-success-email.handler');
|
||||
|
||||
await handler.run({ payload });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendPasswordResetSuccessEmailJobDefinition
|
||||
>;
|
||||
@ -0,0 +1,117 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentRecipientSignedEmailTemplate } from '@documenso/email/templates/document-recipient-signed';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendRecipientSignedEmailJobDefinition } from './send-recipient-signed-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TSendRecipientSignedEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const { documentId, recipientId } = payload;
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const isRecipientSignedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).recipientSigned;
|
||||
|
||||
if (!isRecipientSignedEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [recipient] = document.recipients;
|
||||
const { email: recipientEmail, name: recipientName } = recipient;
|
||||
const { user: owner } = document;
|
||||
|
||||
const recipientReference = recipientName || recipientEmail;
|
||||
|
||||
// Don't send notification if the owner is the one who signed
|
||||
if (owner.email === recipientEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
const template = createElement(DocumentRecipientSignedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
recipientName,
|
||||
recipientEmail,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
await io.runTask('send-recipient-signed-email', async () => {
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
name: owner.name ?? '',
|
||||
address: owner.email,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`${recipientReference} has signed "${document.title}"`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -1,18 +1,5 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentRecipientSignedEmailTemplate } from '@documenso/email/templates/document-recipient-signed';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_ID = 'send.recipient.signed.email';
|
||||
@ -22,6 +9,10 @@ const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
recipientId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendRecipientSignedEmailJobDefinition = z.infer<
|
||||
typeof SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Recipient Signed Email',
|
||||
@ -31,98 +22,9 @@ export const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION = {
|
||||
schema: SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const { documentId, recipientId } = payload;
|
||||
const handler = await import('./send-recipient-signed-email.handler');
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const isRecipientSignedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).recipientSigned;
|
||||
|
||||
if (!isRecipientSignedEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [recipient] = document.recipients;
|
||||
const { email: recipientEmail, name: recipientName } = recipient;
|
||||
const { user: owner } = document;
|
||||
|
||||
const recipientReference = recipientName || recipientEmail;
|
||||
|
||||
// Don't send notification if the owner is the one who signed
|
||||
if (owner.email === recipientEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
const template = createElement(DocumentRecipientSignedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
recipientName,
|
||||
recipientEmail,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
await io.runTask('send-recipient-signed-email', async () => {
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
name: owner.name ?? '',
|
||||
address: owner.email,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`${recipientReference} has signed "${document.title}"`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
});
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_ID,
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
|
||||
import DocumentRejectionConfirmedEmail from '@documenso/email/templates/document-rejection-confirmed';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
|
||||
@ -1,18 +1,13 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DocumentSource, DocumentStatus, RecipientRole, SendStatus } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import {
|
||||
@ -119,9 +114,11 @@ export const run = async ({
|
||||
emailMessage = customEmail?.message ?? '';
|
||||
|
||||
if (!emailMessage) {
|
||||
const inviterName = user.name || '';
|
||||
|
||||
emailMessage = i18n._(
|
||||
team.teamGlobalSettings?.includeSenderDetails
|
||||
? msg`${user.name} on behalf of "${team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`
|
||||
? msg`${inviterName} on behalf of "${team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`
|
||||
: msg`${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_ID = 'send.team-deleted.email';
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||
import { WEBAPP_BASE_URL } from '../../../constants/app';
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
@ -60,8 +60,8 @@ export const run = async ({
|
||||
`send-team-member-joined-email--${invitedMember.id}_${member.id}`,
|
||||
async () => {
|
||||
const emailContent = createElement(TeamJoinEmailTemplate, {
|
||||
assetBaseUrl: WEBAPP_BASE_URL,
|
||||
baseUrl: WEBAPP_BASE_URL,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
memberName: invitedMember.user.name || '',
|
||||
memberEmail: invitedMember.user.email,
|
||||
teamName: team.name,
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||
import { WEBAPP_BASE_URL } from '../../../constants/app';
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
@ -50,8 +50,8 @@ export const run = async ({
|
||||
for (const member of team.members) {
|
||||
await io.runTask(`send-team-member-left-email--${oldMember.id}_${member.id}`, async () => {
|
||||
const emailContent = createElement(TeamJoinEmailTemplate, {
|
||||
assetBaseUrl: WEBAPP_BASE_URL,
|
||||
baseUrl: WEBAPP_BASE_URL,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
memberName: oldMember.name || '',
|
||||
memberEmail: oldMember.email,
|
||||
teamName: team.name,
|
||||
|
||||
@ -1,14 +1,9 @@
|
||||
import { DocumentStatus, RecipientRole, SigningStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'node:path';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
DocumentStatus,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||
@ -24,8 +19,8 @@ import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../../types/webhook-payload';
|
||||
import { getFile } from '../../../universal/upload/get-file';
|
||||
import { putPdfFile } from '../../../universal/upload/put-file';
|
||||
import { getFileServerSide } from '../../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../../universal/upload/put-file.server';
|
||||
import { fieldsContainUnsignedRequiredField } from '../../../utils/advanced-fields-helpers';
|
||||
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
@ -119,7 +114,7 @@ export const run = async ({
|
||||
documentData.data = documentData.initialData;
|
||||
}
|
||||
|
||||
const pdfData = await getFile(documentData);
|
||||
const pdfData = await getFileServerSide(documentData);
|
||||
|
||||
const certificateData =
|
||||
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
||||
@ -165,7 +160,7 @@ export const run = async ({
|
||||
|
||||
const { name } = path.parse(document.title);
|
||||
|
||||
const documentData = await putPdfFile({
|
||||
const documentData = await putPdfFileServerSide({
|
||||
name: `${name}_signed.pdf`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
||||
|
||||
Reference in New Issue
Block a user