mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 01:01:49 +10:00
chore: merge main
This commit is contained in:
@ -23,6 +23,8 @@ export type CreateDocumentMetaOptions = {
|
||||
password?: string;
|
||||
dateFormat?: string;
|
||||
redirectUrl?: string;
|
||||
emailId?: string | null;
|
||||
emailReplyTo?: string | null;
|
||||
emailSettings?: TDocumentEmailSettings;
|
||||
signingOrder?: DocumentSigningOrder;
|
||||
allowDictateNextSigner?: boolean;
|
||||
@ -46,6 +48,8 @@ export const upsertDocumentMeta = async ({
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
emailId,
|
||||
emailReplyTo,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
@ -54,7 +58,7 @@ export const upsertDocumentMeta = async ({
|
||||
language,
|
||||
requestMetadata,
|
||||
}: CreateDocumentMetaOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
const { documentWhereInput, team } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
@ -75,6 +79,22 @@ export const upsertDocumentMeta = async ({
|
||||
|
||||
const { documentMeta: originalDocumentMeta } = document;
|
||||
|
||||
// Validate the emailId belongs to the organisation.
|
||||
if (emailId) {
|
||||
const email = await prisma.organisationEmail.findFirst({
|
||||
where: {
|
||||
id: emailId,
|
||||
organisationId: team.organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Email not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const upsertedDocumentMeta = await tx.documentMeta.upsert({
|
||||
where: {
|
||||
@ -90,6 +110,8 @@ export const upsertDocumentMeta = async ({
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
emailId,
|
||||
emailReplyTo,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
@ -106,6 +128,8 @@ export const upsertDocumentMeta = async ({
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
emailId,
|
||||
emailReplyTo,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
|
||||
@ -24,6 +24,7 @@ import {
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
@ -134,6 +135,24 @@ export const createDocumentV2 = async ({
|
||||
|
||||
const visibility = determineDocumentVisibility(settings.documentVisibility, teamRole);
|
||||
|
||||
const emailId = meta?.emailId;
|
||||
|
||||
// Validate that the email ID belongs to the organisation.
|
||||
if (emailId) {
|
||||
const email = await prisma.organisationEmail.findFirst({
|
||||
where: {
|
||||
id: emailId,
|
||||
organisationId: team.organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Email not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.create({
|
||||
data: {
|
||||
@ -148,15 +167,7 @@ export const createDocumentV2 = async ({
|
||||
formValues,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
create: {
|
||||
...meta,
|
||||
signingOrder: meta?.signingOrder || undefined,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
language: meta?.language || settings.documentLanguage,
|
||||
typedSignatureEnabled: meta?.typedSignatureEnabled ?? settings.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: meta?.uploadSignatureEnabled ?? settings.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: meta?.drawSignatureEnabled ?? settings.drawSignatureEnabled,
|
||||
},
|
||||
create: extractDerivedDocumentMeta(settings, meta),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
import { prefixedId } from '../../universal/id';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
@ -30,6 +31,7 @@ export type CreateDocumentOptions = {
|
||||
formValues?: Record<string, string | number | boolean>;
|
||||
normalizePdf?: boolean;
|
||||
timezone?: string;
|
||||
userTimezone?: string;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
folderId?: string;
|
||||
};
|
||||
@ -44,6 +46,7 @@ export const createDocument = async ({
|
||||
formValues,
|
||||
requestMetadata,
|
||||
timezone,
|
||||
userTimezone,
|
||||
folderId,
|
||||
}: CreateDocumentOptions) => {
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
@ -101,6 +104,10 @@ export const createDocument = async ({
|
||||
}
|
||||
}
|
||||
|
||||
// userTimezone is last because it's always passed in regardless of the organisation/team settings
|
||||
// for uploads from the frontend
|
||||
const timezoneToUse = timezone || settings.documentTimezone || userTimezone;
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.create({
|
||||
data: {
|
||||
@ -117,13 +124,9 @@ export const createDocument = async ({
|
||||
formValues,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
create: {
|
||||
language: settings.documentLanguage,
|
||||
timezone: timezone,
|
||||
typedSignatureEnabled: settings.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: settings.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: settings.drawSignatureEnabled,
|
||||
},
|
||||
create: extractDerivedDocumentMeta(settings, {
|
||||
timezone: timezoneToUse,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -10,7 +10,6 @@ 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 { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
@ -151,11 +150,13 @@ const handleDocumentOwnerDelete = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
// Soft delete completed documents.
|
||||
@ -232,28 +233,24 @@ const handleDocumentOwnerDelete = async ({
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang,
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: recipient.email,
|
||||
name: recipient.name,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: i18n._(msg`Document Cancelled`),
|
||||
html,
|
||||
text,
|
||||
|
||||
@ -5,7 +5,6 @@ import { DocumentStatus, OrganisationType, RecipientRole, SigningStatus } from '
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import {
|
||||
RECIPIENT_ROLES_DESCRIPTION,
|
||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||
@ -96,12 +95,15 @@ export const resendDocument = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { branding, settings, organisationType } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
});
|
||||
const { branding, emailLanguage, organisationType, senderEmail, replyToEmail } =
|
||||
await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
recipientsToRemind.map(async (recipient) => {
|
||||
@ -109,8 +111,7 @@ export const resendDocument = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
const i18n = await getI18nInstance(lang);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||
|
||||
@ -169,11 +170,11 @@ export const resendDocument = async ({
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, {
|
||||
lang,
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(template, {
|
||||
lang,
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
@ -186,10 +187,8 @@ export const resendDocument = async ({
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: customEmail?.subject
|
||||
? renderCustomEmailTemplate(
|
||||
i18n._(msg`Reminder: ${customEmail.subject}`),
|
||||
|
||||
@ -14,7 +14,6 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email'
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { env } from '../../utils/env';
|
||||
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { formatDocumentsPath } from '../../utils/teams';
|
||||
@ -54,11 +53,13 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const { user: owner } = document;
|
||||
@ -97,18 +98,16 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
downloadLink: documentOwnerDownloadLink,
|
||||
});
|
||||
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang,
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: [
|
||||
@ -117,10 +116,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
address: owner.email,
|
||||
},
|
||||
],
|
||||
from: {
|
||||
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
|
||||
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: i18n._(msg`Signing Complete!`),
|
||||
html,
|
||||
text,
|
||||
@ -174,18 +171,16 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang,
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: [
|
||||
@ -194,10 +189,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
address: recipient.email,
|
||||
},
|
||||
],
|
||||
from: {
|
||||
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
|
||||
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject:
|
||||
isDirectTemplate && document.documentMeta?.subject
|
||||
? renderCustomEmailTemplate(document.documentMeta.subject, customEmailTemplate)
|
||||
|
||||
@ -10,7 +10,6 @@ import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { env } from '../../utils/env';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
@ -44,11 +43,13 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
return;
|
||||
}
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
const { branding, emailLanguage, senderEmail } = await getEmailContext({
|
||||
emailType: 'INTERNAL',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const { email, name } = document.user;
|
||||
@ -61,28 +62,23 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang,
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name: name || '',
|
||||
},
|
||||
from: {
|
||||
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
|
||||
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
|
||||
},
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`Document Deleted!`),
|
||||
html,
|
||||
text,
|
||||
|
||||
@ -9,7 +9,6 @@ import { prisma } from '@documenso/prisma';
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { env } from '../../utils/env';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
@ -46,11 +45,13 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
@ -72,28 +73,24 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang,
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
from: {
|
||||
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
|
||||
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: i18n._(msg`Waiting for others to complete signing.`),
|
||||
html,
|
||||
text,
|
||||
|
||||
@ -9,7 +9,6 @@ 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 { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
@ -41,11 +40,13 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
});
|
||||
}
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
const { branding, settings, senderEmail, replyToEmail } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const { status, user } = document;
|
||||
@ -92,10 +93,8 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
address: recipient.email,
|
||||
name: recipient.name,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: i18n._(msg`Document Cancelled`),
|
||||
html,
|
||||
text,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
@ -128,9 +129,11 @@ export const updateDocument = async ({
|
||||
const isTitleSame = data.title === undefined || data.title === document.title;
|
||||
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
|
||||
const isGlobalAccessSame =
|
||||
documentGlobalAccessAuth === undefined || documentGlobalAccessAuth === newGlobalAccessAuth;
|
||||
documentGlobalAccessAuth === undefined ||
|
||||
isDeepEqual(documentGlobalAccessAuth, newGlobalAccessAuth);
|
||||
const isGlobalActionSame =
|
||||
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
||||
documentGlobalActionAuth === undefined ||
|
||||
isDeepEqual(documentGlobalActionAuth, newGlobalActionAuth);
|
||||
const isDocumentVisibilitySame =
|
||||
data.visibility === undefined || data.visibility === document.visibility;
|
||||
|
||||
|
||||
@ -1,16 +1,34 @@
|
||||
import type { BrandingSettings } from '@documenso/email/providers/branding';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { OrganisationType } from '@documenso/prisma/client';
|
||||
import { type OrganisationClaim, type OrganisationGlobalSettings } from '@documenso/prisma/client';
|
||||
import type {
|
||||
DocumentMeta,
|
||||
EmailDomain,
|
||||
Organisation,
|
||||
OrganisationEmail,
|
||||
OrganisationType,
|
||||
} from '@documenso/prisma/client';
|
||||
import {
|
||||
EmailDomainStatus,
|
||||
type OrganisationClaim,
|
||||
type OrganisationGlobalSettings,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
import { DOCUMENSO_INTERNAL_EMAIL } from '../../constants/email';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import {
|
||||
organisationGlobalSettingsToBranding,
|
||||
teamGlobalSettingsToBranding,
|
||||
} from '../../utils/team-global-settings-to-branding';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { extractDerivedTeamSettings } from '../../utils/teams';
|
||||
|
||||
type GetEmailContextOptions = {
|
||||
type EmailMetaOption = Partial<Pick<DocumentMeta, 'emailId' | 'emailReplyTo' | 'language'>>;
|
||||
|
||||
type BaseGetEmailContextOptions = {
|
||||
/**
|
||||
* The source to extract the email context from.
|
||||
* - "Team" will use the team settings followed by the inherited organisation settings
|
||||
* - "Organisation" will use the organisation settings
|
||||
*/
|
||||
source:
|
||||
| {
|
||||
type: 'team';
|
||||
@ -20,37 +38,112 @@ type GetEmailContextOptions = {
|
||||
type: 'organisation';
|
||||
organisationId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The email type being sent, used to determine what email sender and language to use.
|
||||
* - INTERNAL: Emails to users, such as team invites, etc.
|
||||
* - RECIPIENT: Emails to recipients, such as document sent, document signed, etc.
|
||||
*/
|
||||
emailType: 'INTERNAL' | 'RECIPIENT';
|
||||
};
|
||||
|
||||
type InternalGetEmailContextOptions = BaseGetEmailContextOptions & {
|
||||
emailType: 'INTERNAL';
|
||||
meta?: EmailMetaOption | null;
|
||||
};
|
||||
|
||||
type RecipientGetEmailContextOptions = BaseGetEmailContextOptions & {
|
||||
emailType: 'RECIPIENT';
|
||||
|
||||
/**
|
||||
* Force meta options as a typesafe way to ensure developers don't forget to
|
||||
* pass it in if it is available.
|
||||
*/
|
||||
meta: EmailMetaOption | null;
|
||||
};
|
||||
|
||||
type GetEmailContextOptions = InternalGetEmailContextOptions | RecipientGetEmailContextOptions;
|
||||
|
||||
type EmailContextResponse = {
|
||||
allowedEmails: OrganisationEmail[];
|
||||
branding: BrandingSettings;
|
||||
settings: Omit<OrganisationGlobalSettings, 'id'>;
|
||||
claims: OrganisationClaim;
|
||||
organisationType: OrganisationType;
|
||||
senderEmail: {
|
||||
name: string;
|
||||
address: string;
|
||||
};
|
||||
replyToEmail: string | undefined;
|
||||
emailLanguage: string;
|
||||
};
|
||||
|
||||
export const getEmailContext = async (
|
||||
options: GetEmailContextOptions,
|
||||
): Promise<EmailContextResponse> => {
|
||||
const { source } = options;
|
||||
const { source, meta } = options;
|
||||
|
||||
let emailContext: Omit<EmailContextResponse, 'senderEmail' | 'replyToEmail' | 'emailLanguage'>;
|
||||
|
||||
if (source.type === 'organisation') {
|
||||
emailContext = await handleOrganisationEmailContext(source.organisationId);
|
||||
} else {
|
||||
emailContext = await handleTeamEmailContext(source.teamId);
|
||||
}
|
||||
|
||||
const emailLanguage = meta?.language || emailContext.settings.documentLanguage;
|
||||
|
||||
// Immediate return for internal emails.
|
||||
if (options.emailType === 'INTERNAL') {
|
||||
return {
|
||||
...emailContext,
|
||||
senderEmail: DOCUMENSO_INTERNAL_EMAIL,
|
||||
replyToEmail: undefined,
|
||||
emailLanguage, // Not sure if we want to use this for internal emails.
|
||||
};
|
||||
}
|
||||
|
||||
const replyToEmail = meta?.emailReplyTo || emailContext.settings.emailReplyTo || undefined;
|
||||
const senderEmailId = meta?.emailId || emailContext.settings.emailId;
|
||||
|
||||
const foundSenderEmail = emailContext.allowedEmails.find((email) => email.id === senderEmailId);
|
||||
|
||||
// Reset the emailId to null if not found.
|
||||
if (!foundSenderEmail) {
|
||||
emailContext.settings.emailId = null;
|
||||
}
|
||||
|
||||
const senderEmail = foundSenderEmail
|
||||
? {
|
||||
name: foundSenderEmail.emailName,
|
||||
address: foundSenderEmail.email,
|
||||
}
|
||||
: DOCUMENSO_INTERNAL_EMAIL;
|
||||
|
||||
return {
|
||||
...emailContext,
|
||||
senderEmail,
|
||||
replyToEmail,
|
||||
emailLanguage,
|
||||
};
|
||||
};
|
||||
|
||||
const handleOrganisationEmailContext = async (organisationId: string) => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where:
|
||||
source.type === 'organisation'
|
||||
? {
|
||||
id: source.organisationId,
|
||||
}
|
||||
: {
|
||||
teams: {
|
||||
some: {
|
||||
id: source.teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
include: {
|
||||
subscription: true,
|
||||
organisationClaim: true,
|
||||
organisationGlobalSettings: true,
|
||||
emailDomains: {
|
||||
omit: {
|
||||
privateKey: true,
|
||||
},
|
||||
include: {
|
||||
emails: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -60,27 +153,64 @@ export const getEmailContext = async (
|
||||
|
||||
const claims = organisation.organisationClaim;
|
||||
|
||||
if (source.type === 'organisation') {
|
||||
return {
|
||||
branding: organisationGlobalSettingsToBranding(
|
||||
organisation.organisationGlobalSettings,
|
||||
organisation.id,
|
||||
claims.flags.hidePoweredBy ?? false,
|
||||
),
|
||||
settings: organisation.organisationGlobalSettings,
|
||||
claims,
|
||||
organisationType: organisation.type,
|
||||
};
|
||||
}
|
||||
|
||||
const teamSettings = await getTeamSettings({
|
||||
teamId: source.teamId,
|
||||
});
|
||||
const allowedEmails = getAllowedEmails(organisation);
|
||||
|
||||
return {
|
||||
allowedEmails,
|
||||
branding: organisationGlobalSettingsToBranding(
|
||||
organisation.organisationGlobalSettings,
|
||||
organisation.id,
|
||||
claims.flags.hidePoweredBy ?? false,
|
||||
),
|
||||
settings: organisation.organisationGlobalSettings,
|
||||
claims,
|
||||
organisationType: organisation.type,
|
||||
};
|
||||
};
|
||||
|
||||
const handleTeamEmailContext = async (teamId: number) => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
organisation: {
|
||||
include: {
|
||||
organisationClaim: true,
|
||||
organisationGlobalSettings: true,
|
||||
emailDomains: {
|
||||
omit: {
|
||||
privateKey: true,
|
||||
},
|
||||
include: {
|
||||
emails: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const organisation = team.organisation;
|
||||
const claims = organisation.organisationClaim;
|
||||
|
||||
const allowedEmails = getAllowedEmails(organisation);
|
||||
|
||||
const teamSettings = extractDerivedTeamSettings(
|
||||
organisation.organisationGlobalSettings,
|
||||
team.teamGlobalSettings,
|
||||
);
|
||||
|
||||
return {
|
||||
allowedEmails,
|
||||
branding: teamGlobalSettingsToBranding(
|
||||
teamSettings,
|
||||
source.teamId,
|
||||
teamId,
|
||||
claims.flags.hidePoweredBy ?? false,
|
||||
),
|
||||
settings: teamSettings,
|
||||
@ -88,3 +218,18 @@ export const getEmailContext = async (
|
||||
organisationType: organisation.type,
|
||||
};
|
||||
};
|
||||
|
||||
const getAllowedEmails = (
|
||||
organisation: Organisation & {
|
||||
emailDomains: (Pick<EmailDomain, 'status'> & { emails: OrganisationEmail[] })[];
|
||||
organisationClaim: OrganisationClaim;
|
||||
},
|
||||
) => {
|
||||
if (!organisation.organisationClaim.flags.emailDomains) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return organisation.emailDomains
|
||||
.filter((emailDomain) => emailDomain.status === EmailDomainStatus.ACTIVE)
|
||||
.flatMap((emailDomain) => emailDomain.emails);
|
||||
};
|
||||
|
||||
@ -9,7 +9,6 @@ import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/str
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { OrganisationInviteEmailTemplate } from '@documenso/email/templates/organisation-invite';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { isOrganisationRoleWithinUserHierarchy } from '@documenso/lib/utils/organisations';
|
||||
@ -190,7 +189,8 @@ export const sendOrganisationMemberInviteEmail = async ({
|
||||
organisationName: organisation.name,
|
||||
});
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
const { branding, emailLanguage, senderEmail } = await getEmailContext({
|
||||
emailType: 'INTERNAL',
|
||||
source: {
|
||||
type: 'organisation',
|
||||
organisationId: organisation.id,
|
||||
@ -199,24 +199,21 @@ export const sendOrganisationMemberInviteEmail = async ({
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, {
|
||||
lang: settings.documentLanguage,
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: settings.documentLanguage,
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(settings.documentLanguage);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`You have been invited to join ${organisation.name} on Documenso`),
|
||||
html,
|
||||
text,
|
||||
|
||||
@ -11,7 +11,6 @@ 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 { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
@ -125,31 +124,29 @@ export const deleteDocumentRecipient = async ({
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding, plainText: true }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: recipientToDelete.email,
|
||||
name: recipientToDelete.name,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: i18n._(msg`You have been removed from a document`),
|
||||
html,
|
||||
text,
|
||||
|
||||
@ -25,7 +25,6 @@ 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 { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { canRecipientBeModified } from '../../utils/recipients';
|
||||
@ -71,13 +70,6 @@ export const setDocumentRecipients = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
@ -97,6 +89,15 @@ export const setDocumentRecipients = async ({
|
||||
throw new Error('Document already complete');
|
||||
}
|
||||
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId,
|
||||
},
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const recipientsHaveActionAuth = recipients.some(
|
||||
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||
);
|
||||
@ -302,24 +303,20 @@ export const setDocumentRecipients = async ({
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding, plainText: true }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: recipient.email,
|
||||
name: recipient.name,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: i18n._(msg`You have been removed from a document`),
|
||||
html,
|
||||
text,
|
||||
|
||||
@ -8,14 +8,12 @@ import { z } from 'zod';
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { ConfirmTeamEmailTemplate } from '@documenso/email/templates/confirm-team-email';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createTokenVerification } from '@documenso/lib/utils/token-verification';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
import { env } from '../../utils/env';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
@ -122,33 +120,28 @@ export const sendTeamEmailVerificationEmail = async (email: string, token: strin
|
||||
token,
|
||||
});
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
const { branding, emailLanguage, senderEmail } = await getEmailContext({
|
||||
emailType: 'INTERNAL',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const lang = settings.documentLanguage as SupportedLanguageCodes;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang,
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
from: senderEmail,
|
||||
subject: i18n._(
|
||||
msg`A request to use your email has been initiated by ${team.name} on Documenso`,
|
||||
),
|
||||
|
||||
@ -5,7 +5,6 @@ import { msg } from '@lingui/core/macro';
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { TeamEmailRemovedTemplate } from '@documenso/email/templates/team-email-removed';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -27,7 +26,8 @@ export type DeleteTeamEmailOptions = {
|
||||
* The user must either be part of the team with the required permissions, or the owner of the email.
|
||||
*/
|
||||
export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamEmailOptions) => {
|
||||
const { branding, settings } = await getEmailContext({
|
||||
const { branding, emailLanguage, senderEmail } = await getEmailContext({
|
||||
emailType: 'INTERNAL',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId,
|
||||
@ -82,24 +82,19 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE
|
||||
teamUrl: team.url,
|
||||
});
|
||||
|
||||
const lang = settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding, plainText: true }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: team.organisation.owner.email,
|
||||
name: team.organisation.owner.name ?? '',
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`Team email has been revoked for ${team.name}`),
|
||||
html,
|
||||
text,
|
||||
|
||||
@ -7,7 +7,6 @@ import { uniqueBy } from 'remeda';
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { TeamDeleteEmailTemplate } from '@documenso/email/templates/team-delete';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -130,28 +129,24 @@ export const sendTeamDeleteEmail = async ({
|
||||
teamUrl: team.url,
|
||||
});
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
const { branding, emailLanguage, senderEmail } = await getEmailContext({
|
||||
emailType: 'INTERNAL',
|
||||
source: {
|
||||
type: 'organisation',
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
const lang = settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding, plainText: true }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`Team "${team.name}" has been deleted on Documenso`),
|
||||
html,
|
||||
text,
|
||||
|
||||
@ -33,5 +33,13 @@ export const getTeamSettings = async ({ userId, teamId }: GetTeamSettingsOptions
|
||||
const organisationSettings = team.organisation.organisationGlobalSettings;
|
||||
const teamSettings = team.teamGlobalSettings;
|
||||
|
||||
// Override branding settings if inherit is enabled.
|
||||
if (teamSettings.brandingEnabled === null) {
|
||||
teamSettings.brandingEnabled = organisationSettings.brandingEnabled;
|
||||
teamSettings.brandingLogo = organisationSettings.brandingLogo;
|
||||
teamSettings.brandingUrl = organisationSettings.brandingUrl;
|
||||
teamSettings.brandingCompanyDetails = organisationSettings.brandingCompanyDetails;
|
||||
}
|
||||
|
||||
return extractDerivedTeamSettings(organisationSettings, teamSettings);
|
||||
};
|
||||
|
||||
@ -3,7 +3,6 @@ import { createElement } from 'react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { Field, Signature } from '@prisma/client';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
FieldType,
|
||||
@ -25,8 +24,6 @@ import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/f
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../../constants/time-zones';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { TRecipientActionAuthTypes } from '../../types/document-auth';
|
||||
@ -38,6 +35,7 @@ import {
|
||||
} from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { isRequiredField } from '../../utils/advanced-fields-helpers';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import {
|
||||
@ -45,7 +43,6 @@ import {
|
||||
createRecipientAuthOptions,
|
||||
extractDocumentAuthMethods,
|
||||
} from '../../utils/document-auth';
|
||||
import { env } from '../../utils/env';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { formatDocumentsPath } from '../../utils/teams';
|
||||
import { sendDocument } from '../document/send-document';
|
||||
@ -116,7 +113,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Invalid or missing template' });
|
||||
}
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
const { branding, settings, senderEmail, emailLanguage } = await getEmailContext({
|
||||
emailType: 'INTERNAL',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: template.teamId,
|
||||
@ -169,13 +167,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
const nonDirectTemplateRecipients = template.recipients.filter(
|
||||
(recipient) => recipient.id !== directTemplateRecipient.id,
|
||||
);
|
||||
|
||||
const metaTimezone = template.templateMeta?.timezone || DEFAULT_DOCUMENT_TIME_ZONE;
|
||||
const metaDateFormat = template.templateMeta?.dateFormat || DEFAULT_DOCUMENT_DATE_FORMAT;
|
||||
const metaEmailMessage = template.templateMeta?.message || '';
|
||||
const metaEmailSubject = template.templateMeta?.subject || '';
|
||||
const metaLanguage = template.templateMeta?.language ?? settings.documentLanguage;
|
||||
const metaSigningOrder = template.templateMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
|
||||
const derivedDocumentMeta = extractDerivedDocumentMeta(settings, template.templateMeta);
|
||||
|
||||
// Associate, validate and map to a query every direct template recipient field with the provided fields.
|
||||
// Only process fields that are either required or have been signed by the user
|
||||
@ -234,7 +226,9 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
const typedSignature = isSignatureField && !isBase64 ? value : undefined;
|
||||
|
||||
if (templateField.type === FieldType.DATE) {
|
||||
customText = DateTime.now().setZone(metaTimezone).toFormat(metaDateFormat);
|
||||
customText = DateTime.now()
|
||||
.setZone(derivedDocumentMeta.timezone)
|
||||
.toFormat(derivedDocumentMeta.dateFormat);
|
||||
}
|
||||
|
||||
if (isSignatureField && !signatureImageAsBase64 && !typedSignature) {
|
||||
@ -318,18 +312,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
},
|
||||
},
|
||||
documentMeta: {
|
||||
create: {
|
||||
timezone: metaTimezone,
|
||||
dateFormat: metaDateFormat,
|
||||
message: metaEmailMessage,
|
||||
subject: metaEmailSubject,
|
||||
language: metaLanguage,
|
||||
signingOrder: metaSigningOrder,
|
||||
distributionMethod: template.templateMeta?.distributionMethod,
|
||||
typedSignatureEnabled: template.templateMeta?.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: template.templateMeta?.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: template.templateMeta?.drawSignatureEnabled,
|
||||
},
|
||||
create: derivedDocumentMeta,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
@ -589,11 +572,11 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailTemplate, { lang: metaLanguage, branding }),
|
||||
renderEmailWithI18N(emailTemplate, { lang: metaLanguage, branding, plainText: true }),
|
||||
renderEmailWithI18N(emailTemplate, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(emailTemplate, { lang: emailLanguage, branding, plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(metaLanguage);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: [
|
||||
@ -602,10 +585,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
address: templateOwner.email,
|
||||
},
|
||||
],
|
||||
from: {
|
||||
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
|
||||
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
|
||||
},
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`Document created from direct template`),
|
||||
html,
|
||||
text,
|
||||
|
||||
@ -3,6 +3,7 @@ import { DocumentSource, type RecipientRole } from '@prisma/client';
|
||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
|
||||
@ -78,18 +79,7 @@ export const createDocumentFromTemplateLegacy = async ({
|
||||
})),
|
||||
},
|
||||
documentMeta: {
|
||||
create: {
|
||||
subject: template.templateMeta?.subject,
|
||||
message: template.templateMeta?.message,
|
||||
timezone: template.templateMeta?.timezone,
|
||||
dateFormat: template.templateMeta?.dateFormat,
|
||||
redirectUrl: template.templateMeta?.redirectUrl,
|
||||
signingOrder: template.templateMeta?.signingOrder ?? undefined,
|
||||
language: template.templateMeta?.language || settings.documentLanguage,
|
||||
typedSignatureEnabled: template.templateMeta?.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: template.templateMeta?.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: template.templateMeta?.drawSignatureEnabled,
|
||||
},
|
||||
create: extractDerivedDocumentMeta(settings, template.templateMeta),
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { DocumentDistributionMethod } from '@prisma/client';
|
||||
import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentSource,
|
||||
type Field,
|
||||
type Recipient,
|
||||
@ -40,6 +39,7 @@ import {
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import {
|
||||
createDocumentAuthOptions,
|
||||
@ -388,7 +388,7 @@ export const createDocumentFromTemplate = async ({
|
||||
})),
|
||||
},
|
||||
documentMeta: {
|
||||
create: {
|
||||
create: extractDerivedDocumentMeta(settings, {
|
||||
subject: override?.subject || template.templateMeta?.subject,
|
||||
message: override?.message || template.templateMeta?.message,
|
||||
timezone: override?.timezone || template.templateMeta?.timezone,
|
||||
@ -397,13 +397,8 @@ export const createDocumentFromTemplate = async ({
|
||||
redirectUrl: override?.redirectUrl || template.templateMeta?.redirectUrl,
|
||||
distributionMethod:
|
||||
override?.distributionMethod || template.templateMeta?.distributionMethod,
|
||||
// last `undefined` is due to JsonValue's
|
||||
emailSettings:
|
||||
override?.emailSettings || template.templateMeta?.emailSettings || undefined,
|
||||
signingOrder:
|
||||
override?.signingOrder ||
|
||||
template.templateMeta?.signingOrder ||
|
||||
DocumentSigningOrder.PARALLEL,
|
||||
emailSettings: override?.emailSettings || template.templateMeta?.emailSettings,
|
||||
signingOrder: override?.signingOrder || template.templateMeta?.signingOrder,
|
||||
language:
|
||||
override?.language || template.templateMeta?.language || settings.documentLanguage,
|
||||
typedSignatureEnabled:
|
||||
@ -413,10 +408,8 @@ export const createDocumentFromTemplate = async ({
|
||||
drawSignatureEnabled:
|
||||
override?.drawSignatureEnabled ?? template.templateMeta?.drawSignatureEnabled,
|
||||
allowDictateNextSigner:
|
||||
override?.allowDictateNextSigner ??
|
||||
template.templateMeta?.allowDictateNextSigner ??
|
||||
false,
|
||||
},
|
||||
override?.allowDictateNextSigner ?? template.templateMeta?.allowDictateNextSigner,
|
||||
}),
|
||||
},
|
||||
recipients: {
|
||||
createMany: {
|
||||
|
||||
@ -6,6 +6,7 @@ import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema//Tem
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import { createDocumentAuthOptions } from '../../utils/document-auth';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
@ -69,6 +70,24 @@ export const createTemplate = async ({
|
||||
teamId,
|
||||
});
|
||||
|
||||
const emailId = meta.emailId;
|
||||
|
||||
// Validate that the email ID belongs to the organisation.
|
||||
if (emailId) {
|
||||
const email = await prisma.organisationEmail.findFirst({
|
||||
where: {
|
||||
id: emailId,
|
||||
organisationId: team.organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Email not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.template.create({
|
||||
data: {
|
||||
title,
|
||||
@ -86,14 +105,7 @@ export const createTemplate = async ({
|
||||
publicDescription: data.publicDescription,
|
||||
type: data.type,
|
||||
templateMeta: {
|
||||
create: {
|
||||
...meta,
|
||||
language: meta?.language ?? settings.documentLanguage,
|
||||
typedSignatureEnabled: meta?.typedSignatureEnabled ?? settings.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: meta?.uploadSignatureEnabled ?? settings.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: meta?.drawSignatureEnabled ?? settings.drawSignatureEnabled,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
},
|
||||
create: extractDerivedDocumentMeta(settings, meta),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -41,6 +41,7 @@ export const updateTemplate = async ({
|
||||
templateMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
organisationId: true,
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
@ -86,6 +87,24 @@ export const updateTemplate = async ({
|
||||
globalActionAuth: newGlobalActionAuth,
|
||||
});
|
||||
|
||||
const emailId = meta.emailId;
|
||||
|
||||
// Validate the emailId belongs to the organisation.
|
||||
if (emailId) {
|
||||
const email = await prisma.organisationEmail.findFirst({
|
||||
where: {
|
||||
id: emailId,
|
||||
organisationId: template.team.organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Email not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.template.update({
|
||||
where: {
|
||||
id: templateId,
|
||||
|
||||
Reference in New Issue
Block a user