mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 18:51:37 +10:00
fix: merge conflicts
This commit is contained in:
@ -13,6 +13,7 @@ export const getRecipientsStats = async () => {
|
||||
[ReadStatus.NOT_OPENED]: 0,
|
||||
[SigningStatus.SIGNED]: 0,
|
||||
[SigningStatus.NOT_SIGNED]: 0,
|
||||
[SigningStatus.REJECTED]: 0,
|
||||
[SendStatus.SENT]: 0,
|
||||
[SendStatus.NOT_SENT]: 0,
|
||||
};
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import { ConfirmEmailTemplate } from '@documenso/email/templates/confirm-email';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
|
||||
export interface SendConfirmationEmailProps {
|
||||
userId: number;
|
||||
@ -45,6 +48,13 @@ export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailPro
|
||||
confirmationLink,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(confirmationTemplate),
|
||||
renderEmailWithI18N(confirmationTemplate, { plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance();
|
||||
|
||||
return mailer.sendMail({
|
||||
to: {
|
||||
address: user.email,
|
||||
@ -54,8 +64,8 @@ export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailPro
|
||||
name: senderName,
|
||||
address: senderAddress,
|
||||
},
|
||||
subject: 'Please confirm your email',
|
||||
html: render(confirmationTemplate),
|
||||
text: render(confirmationTemplate, { plainText: true }),
|
||||
subject: i18n._(msg`Please confirm your email`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import { ForgotPasswordTemplate } from '@documenso/email/templates/forgot-password';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
|
||||
export interface SendForgotPasswordOptions {
|
||||
userId: number;
|
||||
@ -39,6 +42,13 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
|
||||
resetPasswordLink,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template),
|
||||
renderEmailWithI18N(template, { plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance();
|
||||
|
||||
return await mailer.sendMail({
|
||||
to: {
|
||||
address: user.email,
|
||||
@ -48,8 +58,8 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
|
||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||
},
|
||||
subject: 'Forgot Password?',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
subject: i18n._(msg`Forgot Password?`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import { ResetPasswordTemplate } from '@documenso/email/templates/reset-password';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
|
||||
export interface SendResetPasswordOptions {
|
||||
userId: number;
|
||||
@ -26,6 +26,11 @@ export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) =>
|
||||
userName: user.name || '',
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template),
|
||||
renderEmailWithI18N(template, { plainText: true }),
|
||||
]);
|
||||
|
||||
return await mailer.sendMail({
|
||||
to: {
|
||||
address: user.email,
|
||||
@ -36,7 +41,7 @@ export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) =>
|
||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||
},
|
||||
subject: 'Password Reset Success!',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
|
||||
@ -7,7 +7,10 @@ import {
|
||||
diffDocumentMetaChanges,
|
||||
} from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentSigningOrder } from '@documenso/prisma/client';
|
||||
import type { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
||||
|
||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||
|
||||
export type CreateDocumentMetaOptions = {
|
||||
documentId: number;
|
||||
@ -17,8 +20,11 @@ export type CreateDocumentMetaOptions = {
|
||||
password?: string;
|
||||
dateFormat?: string;
|
||||
redirectUrl?: string;
|
||||
emailSettings?: TDocumentEmailSettings;
|
||||
signingOrder?: DocumentSigningOrder;
|
||||
distributionMethod?: DocumentDistributionMethod;
|
||||
typedSignatureEnabled?: boolean;
|
||||
language?: SupportedLanguageCodes;
|
||||
userId: number;
|
||||
requestMetadata: RequestMetadata;
|
||||
};
|
||||
@ -33,7 +39,10 @@ export const upsertDocumentMeta = async ({
|
||||
userId,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
language,
|
||||
requestMetadata,
|
||||
}: CreateDocumentMetaOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
@ -84,7 +93,10 @@ export const upsertDocumentMeta = async ({
|
||||
documentId,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
language,
|
||||
},
|
||||
update: {
|
||||
subject,
|
||||
@ -94,7 +106,10 @@ export const upsertDocumentMeta = async ({
|
||||
timezone,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
language,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -5,7 +5,9 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentSource, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
import { DocumentSource, DocumentVisibility, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
@ -48,6 +50,51 @@ export const createDocument = async ({
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, 'Team not found');
|
||||
}
|
||||
|
||||
let team: (Team & { teamGlobalSettings: TeamGlobalSettings | null }) | null = null;
|
||||
let userTeamRole: TeamMemberRole | undefined;
|
||||
|
||||
if (teamId) {
|
||||
const teamWithUserRole = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
members: {
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
team = teamWithUserRole;
|
||||
userTeamRole = teamWithUserRole.members[0]?.role;
|
||||
}
|
||||
|
||||
const determineVisibility = (
|
||||
globalVisibility: DocumentVisibility | null | undefined,
|
||||
userRole: TeamMemberRole,
|
||||
): DocumentVisibility => {
|
||||
const defaultVisibility = globalVisibility ?? DocumentVisibility.EVERYONE;
|
||||
|
||||
if (userRole === TeamMemberRole.ADMIN) {
|
||||
return defaultVisibility;
|
||||
}
|
||||
|
||||
if (userRole === TeamMemberRole.MANAGER) {
|
||||
if (defaultVisibility === DocumentVisibility.ADMIN) {
|
||||
return DocumentVisibility.MANAGER_AND_ABOVE;
|
||||
}
|
||||
return defaultVisibility;
|
||||
}
|
||||
|
||||
return DocumentVisibility.EVERYONE;
|
||||
};
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.create({
|
||||
data: {
|
||||
@ -56,8 +103,17 @@ export const createDocument = async ({
|
||||
documentDataId,
|
||||
userId,
|
||||
teamId,
|
||||
visibility: determineVisibility(
|
||||
team?.teamGlobalSettings?.documentVisibility,
|
||||
userTeamRole ?? TeamMemberRole.MEMBER,
|
||||
),
|
||||
formValues,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
create: {
|
||||
language: team?.teamGlobalSettings?.documentLanguage,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -2,18 +2,30 @@
|
||||
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Document, DocumentMeta, Recipient, User } from '@documenso/prisma/client';
|
||||
import type {
|
||||
Document,
|
||||
DocumentMeta,
|
||||
Recipient,
|
||||
Team,
|
||||
TeamGlobalSettings,
|
||||
User,
|
||||
} from '@documenso/prisma/client';
|
||||
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||
|
||||
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 { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
|
||||
export type DeleteDocumentOptions = {
|
||||
id: number;
|
||||
@ -46,8 +58,9 @@ export const deleteDocument = async ({
|
||||
Recipient: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
include: {
|
||||
members: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -70,6 +83,7 @@ export const deleteDocument = async ({
|
||||
await handleDocumentOwnerDelete({
|
||||
document,
|
||||
user,
|
||||
team: document.team,
|
||||
requestMetadata,
|
||||
});
|
||||
}
|
||||
@ -110,6 +124,11 @@ type HandleDocumentOwnerDeleteOptions = {
|
||||
Recipient: Recipient[];
|
||||
documentMeta: DocumentMeta | null;
|
||||
};
|
||||
team?:
|
||||
| (Team & {
|
||||
teamGlobalSettings?: TeamGlobalSettings | null;
|
||||
})
|
||||
| null;
|
||||
user: User;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
@ -117,6 +136,7 @@ type HandleDocumentOwnerDeleteOptions = {
|
||||
const handleDocumentOwnerDelete = async ({
|
||||
document,
|
||||
user,
|
||||
team,
|
||||
requestMetadata,
|
||||
}: HandleDocumentOwnerDeleteOptions) => {
|
||||
if (document.deletedAt) {
|
||||
@ -175,6 +195,14 @@ const handleDocumentOwnerDelete = async ({
|
||||
});
|
||||
});
|
||||
|
||||
const isDocumentDeleteEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).documentDeleted;
|
||||
|
||||
if (!isDocumentDeleteEmailEnabled) {
|
||||
return deletedDocument;
|
||||
}
|
||||
|
||||
// Send cancellation emails to recipients.
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
@ -191,6 +219,21 @@ const handleDocumentOwnerDelete = async ({
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const branding = team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(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,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: recipient.email,
|
||||
@ -200,9 +243,9 @@ const handleDocumentOwnerDelete = async ({
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: 'Document Cancelled',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
subject: i18n._(msg`Document Cancelled`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
@ -66,6 +66,7 @@ export const findDocumentAuditLogs = async ({
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
|
||||
],
|
||||
|
||||
@ -3,7 +3,14 @@ import { P, match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import type { Document, Prisma, Team, TeamEmail, User } from '@documenso/prisma/client';
|
||||
import type {
|
||||
Document,
|
||||
DocumentSource,
|
||||
Prisma,
|
||||
Team,
|
||||
TeamEmail,
|
||||
User,
|
||||
} from '@documenso/prisma/client';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
@ -16,6 +23,8 @@ export type FindDocumentsOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
term?: string;
|
||||
templateId?: number;
|
||||
source?: DocumentSource;
|
||||
status?: ExtendedDocumentStatus;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
@ -32,6 +41,8 @@ export const findDocuments = async ({
|
||||
userId,
|
||||
teamId,
|
||||
term,
|
||||
templateId,
|
||||
source,
|
||||
status = ExtendedDocumentStatus.ALL,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
@ -40,44 +51,37 @@ export const findDocuments = async ({
|
||||
senderIds,
|
||||
search,
|
||||
}: FindDocumentsOptions) => {
|
||||
const { user, team } = await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.findFirstOrThrow({
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
let team = null;
|
||||
|
||||
if (teamId !== undefined) {
|
||||
team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamEmail: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let team = null;
|
||||
|
||||
if (teamId !== undefined) {
|
||||
team = await tx.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamEmail: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
team,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
@ -120,11 +124,18 @@ export const findDocuments = async ({
|
||||
}))
|
||||
.otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })),
|
||||
{
|
||||
Recipient: {
|
||||
some: {
|
||||
email: user.email,
|
||||
OR: [
|
||||
{
|
||||
Recipient: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@ -197,8 +208,27 @@ export const findDocuments = async ({
|
||||
};
|
||||
}
|
||||
|
||||
const whereAndClause: Prisma.DocumentWhereInput['AND'] = [
|
||||
{ ...termFilters },
|
||||
{ ...filters },
|
||||
{ ...deletedFilter },
|
||||
{ ...searchFilter },
|
||||
];
|
||||
|
||||
if (templateId) {
|
||||
whereAndClause.push({
|
||||
templateId,
|
||||
});
|
||||
}
|
||||
|
||||
if (source) {
|
||||
whereAndClause.push({
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
const whereClause: Prisma.DocumentWhereInput = {
|
||||
AND: [{ ...termFilters }, { ...filters }, { ...deletedFilter }, { ...searchFilter }],
|
||||
AND: whereAndClause,
|
||||
};
|
||||
|
||||
if (period) {
|
||||
|
||||
@ -143,11 +143,18 @@ export const getDocumentWhereInput = async ({
|
||||
])
|
||||
.otherwise(() => [{ visibility: DocumentVisibility.EVERYONE }]),
|
||||
{
|
||||
Recipient: {
|
||||
some: {
|
||||
email: user.email,
|
||||
OR: [
|
||||
{
|
||||
Recipient: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -6,11 +6,10 @@ import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import type { Prisma, User } from '@documenso/prisma/client';
|
||||
import { SigningStatus } from '@documenso/prisma/client';
|
||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
|
||||
export type GetStatsInput = {
|
||||
user: User;
|
||||
team?: Omit<GetTeamCountsOption, 'createdAt'>;
|
||||
@ -207,47 +206,45 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
let notSignedCountsGroupByArgs = null;
|
||||
let hasSignedCountsGroupByArgs = null;
|
||||
|
||||
const visibilityFilters = [
|
||||
...match(options.currentTeamMemberRole)
|
||||
.with(TeamMemberRole.ADMIN, () => [
|
||||
{ visibility: DocumentVisibility.EVERYONE },
|
||||
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
|
||||
{ visibility: DocumentVisibility.ADMIN },
|
||||
])
|
||||
.with(TeamMemberRole.MANAGER, () => [
|
||||
{ visibility: DocumentVisibility.EVERYONE },
|
||||
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
|
||||
])
|
||||
.otherwise(() => [{ visibility: DocumentVisibility.EVERYONE }]),
|
||||
];
|
||||
|
||||
ownerCountsWhereInput = {
|
||||
...ownerCountsWhereInput,
|
||||
OR: [
|
||||
const visibilityFiltersWhereInput: Prisma.DocumentWhereInput = {
|
||||
AND: [
|
||||
{ deletedAt: null },
|
||||
{
|
||||
AND: [
|
||||
{
|
||||
visibility: {
|
||||
in: visibilityFilters.map((filter) => filter.visibility),
|
||||
},
|
||||
},
|
||||
{
|
||||
Recipient: {
|
||||
none: {
|
||||
email: options.currentUserEmail,
|
||||
OR: [
|
||||
match(options.currentTeamMemberRole)
|
||||
.with(TeamMemberRole.ADMIN, () => ({
|
||||
visibility: {
|
||||
in: [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
DocumentVisibility.ADMIN,
|
||||
],
|
||||
},
|
||||
},
|
||||
}))
|
||||
.with(TeamMemberRole.MANAGER, () => ({
|
||||
visibility: {
|
||||
in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE],
|
||||
},
|
||||
}))
|
||||
.otherwise(() => ({
|
||||
visibility: {
|
||||
equals: DocumentVisibility.EVERYONE,
|
||||
},
|
||||
})),
|
||||
{
|
||||
OR: [
|
||||
{ userId: options.userId },
|
||||
{ Recipient: { some: { email: options.currentUserEmail } } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
Recipient: {
|
||||
some: {
|
||||
email: options.currentUserEmail,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
ownerCountsWhereInput = {
|
||||
...ownerCountsWhereInput,
|
||||
...visibilityFiltersWhereInput,
|
||||
...searchFilter,
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,92 @@
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { jobs } from '@documenso/lib/jobs/client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export type RejectDocumentWithTokenOptions = {
|
||||
token: string;
|
||||
documentId: number;
|
||||
reason: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export async function rejectDocumentWithToken({
|
||||
token,
|
||||
documentId,
|
||||
reason,
|
||||
requestMetadata,
|
||||
}: RejectDocumentWithTokenOptions) {
|
||||
// Find the recipient and document in a single query
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
token,
|
||||
documentId,
|
||||
},
|
||||
include: {
|
||||
Document: {
|
||||
include: {
|
||||
User: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const document = recipient?.Document;
|
||||
|
||||
if (!recipient || !document) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Document or recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Add the audit log entry before updating the recipient
|
||||
|
||||
// Update the recipient status to rejected
|
||||
const [updatedRecipient] = await prisma.$transaction([
|
||||
prisma.recipient.update({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
data: {
|
||||
signedAt: new Date(),
|
||||
signingStatus: SigningStatus.REJECTED,
|
||||
rejectionReason: reason,
|
||||
},
|
||||
}),
|
||||
prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
},
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
reason,
|
||||
},
|
||||
requestMetadata,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
// Send email notifications
|
||||
await jobs.triggerJob({
|
||||
name: 'send.signing.rejected.emails',
|
||||
payload: {
|
||||
recipientId: recipient.id,
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
return updatedRecipient;
|
||||
}
|
||||
@ -1,11 +1,12 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
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 {
|
||||
RECIPIENT_ROLES_DESCRIPTION_ENG,
|
||||
RECIPIENT_ROLES_DESCRIPTION,
|
||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||
} from '@documenso/lib/constants/recipient-roles';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
@ -16,7 +17,11 @@ import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export type ResendDocumentOptions = {
|
||||
@ -62,6 +67,7 @@ export const resendDocument = async ({
|
||||
select: {
|
||||
teamEmail: true,
|
||||
name: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -86,31 +92,50 @@ export const resendDocument = async ({
|
||||
throw new Error('Can not send completed document');
|
||||
}
|
||||
|
||||
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).recipientSigningRequest;
|
||||
|
||||
if (!isRecipientSigningRequestEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
if (recipient.role === RecipientRole.CC) {
|
||||
return;
|
||||
}
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||
|
||||
const { email, name } = recipient;
|
||||
const selfSigner = email === user.email;
|
||||
|
||||
const recipientActionVerb =
|
||||
RECIPIENT_ROLES_DESCRIPTION_ENG[recipient.role].actionVerb.toLowerCase();
|
||||
const recipientActionVerb = i18n
|
||||
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
|
||||
.toLowerCase();
|
||||
|
||||
let emailMessage = customEmail?.message || '';
|
||||
let emailSubject = `Reminder: Please ${recipientActionVerb} this document`;
|
||||
let emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} this document`);
|
||||
|
||||
if (selfSigner) {
|
||||
emailMessage = `You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`;
|
||||
emailSubject = `Reminder: Please ${recipientActionVerb} your document`;
|
||||
emailMessage = i18n._(
|
||||
msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`,
|
||||
);
|
||||
emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} your document`);
|
||||
}
|
||||
|
||||
if (isTeamDocument && document.team) {
|
||||
emailSubject = `Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`;
|
||||
emailMessage = `${user.name} on behalf of ${document.team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`;
|
||||
emailSubject = i18n._(
|
||||
msg`Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`,
|
||||
);
|
||||
emailMessage =
|
||||
customEmail?.message ||
|
||||
i18n._(
|
||||
msg`${user.name} on behalf of ${document.team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
|
||||
);
|
||||
}
|
||||
|
||||
const customEmailTemplate = {
|
||||
@ -135,8 +160,24 @@ export const resendDocument = async ({
|
||||
teamName: document.team?.name,
|
||||
});
|
||||
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
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: {
|
||||
address: email,
|
||||
@ -147,10 +188,13 @@ export const resendDocument = async ({
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: customEmail?.subject
|
||||
? renderCustomEmailTemplate(`Reminder: ${customEmail.subject}`, customEmailTemplate)
|
||||
? renderCustomEmailTemplate(
|
||||
i18n._(msg`Reminder: ${customEmail.subject}`),
|
||||
customEmailTemplate,
|
||||
)
|
||||
: emailSubject,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
|
||||
@ -10,6 +10,7 @@ import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import { ZSupportedLanguageCodeSchema } from '../../constants/i18n';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putPdfFile } from '../../universal/upload/put-file';
|
||||
@ -45,6 +46,7 @@ export const sealDocument = async ({
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
@ -90,7 +92,9 @@ export const sealDocument = async ({
|
||||
// !: Need to write the fields onto the document as a hard copy
|
||||
const pdfData = await getFile(documentData);
|
||||
|
||||
const certificate = await getCertificatePdf({ documentId })
|
||||
const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
|
||||
|
||||
const certificate = await getCertificatePdf({ documentId, language: documentLanguage })
|
||||
.then(async (doc) => PDFDocument.load(doc))
|
||||
.catch(() => null);
|
||||
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
export type SearchDocumentsWithKeywordOptions = {
|
||||
query: string;
|
||||
@ -67,10 +71,40 @@ export const searchDocumentsWithKeyword = async ({
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
title: {
|
||||
contains: query,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
teamId: {
|
||||
not: null,
|
||||
},
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
team: {
|
||||
select: {
|
||||
url: true,
|
||||
members: {
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
@ -82,15 +116,48 @@ export const searchDocumentsWithKeyword = async ({
|
||||
const getSigningLink = (recipients: Recipient[], user: User) =>
|
||||
`/sign/${recipients.find((r) => r.email === user.email)?.token}`;
|
||||
|
||||
const maskedDocuments = documents.map((document) => {
|
||||
const { Recipient, ...documentWithoutRecipient } = document;
|
||||
const maskedDocuments = documents
|
||||
.filter((document) => {
|
||||
if (!document.teamId || isOwner(document, user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
...documentWithoutRecipient,
|
||||
path: isOwner(document, user) ? `/documents/${document.id}` : getSigningLink(Recipient, user),
|
||||
value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '),
|
||||
};
|
||||
});
|
||||
const teamMemberRole = document.team?.members[0]?.role;
|
||||
|
||||
if (!teamMemberRole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const canAccessDocument = match([document.visibility, teamMemberRole])
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true)
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true)
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MEMBER], () => true)
|
||||
.with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.ADMIN], () => true)
|
||||
.with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.MANAGER], () => true)
|
||||
.with([DocumentVisibility.ADMIN, TeamMemberRole.ADMIN], () => true)
|
||||
.otherwise(() => false);
|
||||
|
||||
return canAccessDocument;
|
||||
})
|
||||
.map((document) => {
|
||||
const { Recipient, ...documentWithoutRecipient } = document;
|
||||
|
||||
let documentPath;
|
||||
|
||||
if (isOwner(document, user)) {
|
||||
documentPath = `${formatDocumentsPath(document.team?.url)}/${document.id}`;
|
||||
} else if (document.teamId && document.team) {
|
||||
documentPath = `${formatDocumentsPath(document.team.url)}/${document.id}`;
|
||||
} else {
|
||||
documentPath = getSigningLink(Recipient, user);
|
||||
}
|
||||
|
||||
return {
|
||||
...documentWithoutRecipient,
|
||||
path: documentPath,
|
||||
value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '),
|
||||
};
|
||||
});
|
||||
|
||||
return maskedDocuments;
|
||||
};
|
||||
|
||||
@ -1,17 +1,23 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentSource } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { formatDocumentsPath } from '../../utils/teams';
|
||||
|
||||
export interface SendDocumentOptions {
|
||||
documentId: number;
|
||||
@ -32,6 +38,7 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -53,7 +60,9 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
let documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/documents/${document.id}`;
|
||||
let documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(
|
||||
document.team?.url,
|
||||
)}/${document.id}`;
|
||||
|
||||
if (document.team?.url) {
|
||||
documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${document.team.url}/documents/${
|
||||
@ -61,14 +70,36 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
}`;
|
||||
}
|
||||
|
||||
// If the document owner is not a recipient then send the email to them separately
|
||||
if (!document.Recipient.find((recipient) => recipient.email === owner.email)) {
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
const isDocumentCompletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).documentCompleted;
|
||||
|
||||
// If the document owner is not a recipient, OR recipient emails are disabled, then send the email to them separately.
|
||||
if (
|
||||
!document.Recipient.find((recipient) => recipient.email === owner.email) ||
|
||||
!isDocumentCompletedEmailEnabled
|
||||
) {
|
||||
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
assetBaseUrl,
|
||||
downloadLink: documentOwnerDownloadLink,
|
||||
});
|
||||
|
||||
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: [
|
||||
{
|
||||
@ -80,9 +111,9 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||
},
|
||||
subject: 'Signing Complete!',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
subject: i18n._(msg`Signing Complete!`),
|
||||
html,
|
||||
text,
|
||||
attachments: [
|
||||
{
|
||||
filename: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
|
||||
@ -109,6 +140,10 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
});
|
||||
}
|
||||
|
||||
if (!isDocumentCompletedEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
const customEmailTemplate = {
|
||||
@ -129,6 +164,19 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
: undefined,
|
||||
});
|
||||
|
||||
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: [
|
||||
{
|
||||
@ -143,9 +191,9 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
subject:
|
||||
isDirectTemplate && document.documentMeta?.subject
|
||||
? renderCustomEmailTemplate(document.documentMeta.subject, customEmailTemplate)
|
||||
: 'Signing Complete!',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
: i18n._(msg`Signing Complete!`),
|
||||
html,
|
||||
text,
|
||||
attachments: [
|
||||
{
|
||||
filename: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import { DocumentSuperDeleteEmailTemplate } from '@documenso/email/templates/document-super-delete';
|
||||
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 { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
|
||||
export interface SendDeleteEmailOptions {
|
||||
documentId: number;
|
||||
@ -19,6 +24,12 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
},
|
||||
include: {
|
||||
User: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -26,6 +37,14 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).documentDeleted;
|
||||
|
||||
if (!isDocumentDeletedEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { email, name } = document.User;
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
@ -36,6 +55,21 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
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,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance();
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
@ -45,8 +79,8 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||
},
|
||||
subject: 'Document Deleted!',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
subject: i18n._(msg`Document Deleted!`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { jobs } from '../../jobs/client';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
@ -29,7 +30,7 @@ export const sendDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
sendEmail = true,
|
||||
sendEmail,
|
||||
requestMetadata,
|
||||
}: SendDocumentOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
@ -156,7 +157,14 @@ export const sendDocument = async ({
|
||||
// throw new Error('Some signers have not been assigned a signature field.');
|
||||
// }
|
||||
|
||||
if (sendEmail) {
|
||||
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).recipientSigningRequest;
|
||||
|
||||
// Only send email if one of the following is true:
|
||||
// - It is explicitly set
|
||||
// - The email is enabled for signing requests AND sendEmail is undefined
|
||||
if (sendEmail || (isRecipientSigningRequestEmailEnabled && sendEmail === undefined)) {
|
||||
await Promise.all(
|
||||
recipientsToNotify.map(async (recipient) => {
|
||||
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending';
|
||||
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 { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
|
||||
export interface SendPendingEmailOptions {
|
||||
documentId: number;
|
||||
@ -28,6 +33,12 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -39,6 +50,14 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).documentPending;
|
||||
|
||||
if (!isDocumentPendingEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [recipient] = document.Recipient;
|
||||
|
||||
const { email, name } = recipient;
|
||||
@ -50,6 +69,21 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
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,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
@ -59,8 +93,8 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||
},
|
||||
subject: 'Waiting for others to complete signing.',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
subject: i18n._(msg`Waiting for others to complete signing.`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
|
||||
@ -2,17 +2,22 @@
|
||||
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||
|
||||
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 { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
|
||||
export type SuperDeleteDocumentOptions = {
|
||||
id: number;
|
||||
@ -28,6 +33,11 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
Recipient: true,
|
||||
documentMeta: true,
|
||||
User: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -37,8 +47,16 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
|
||||
const { status, User: user } = document;
|
||||
|
||||
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).documentDeleted;
|
||||
|
||||
// if the document is pending, send cancellation emails to all recipients
|
||||
if (status === DocumentStatus.PENDING && document.Recipient.length > 0) {
|
||||
if (
|
||||
status === DocumentStatus.PENDING &&
|
||||
document.Recipient.length > 0 &&
|
||||
isDocumentDeletedEmailEnabled
|
||||
) {
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||
@ -53,6 +71,21 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
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,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: recipient.email,
|
||||
@ -62,9 +95,9 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: 'Document Cancelled',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
subject: i18n._(msg`Document Cancelled`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
'use server';
|
||||
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
@ -20,7 +22,7 @@ export type UpdateDocumentSettingsOptions = {
|
||||
data: {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: string | null;
|
||||
visibility?: DocumentVisibility | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||
};
|
||||
@ -63,8 +65,62 @@ export const updateDocumentSettings = async ({
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (teamId) {
|
||||
const currentUserRole = document.team?.members[0]?.role;
|
||||
|
||||
match(currentUserRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => {
|
||||
const allowedVisibilities: DocumentVisibility[] = [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
];
|
||||
|
||||
if (
|
||||
!allowedVisibilities.includes(document.visibility) ||
|
||||
(data.visibility && !allowedVisibilities.includes(data.visibility))
|
||||
) {
|
||||
throw new AppError(
|
||||
AppErrorCode.UNAUTHORIZED,
|
||||
'You do not have permission to update the document visibility',
|
||||
);
|
||||
}
|
||||
})
|
||||
.with(TeamMemberRole.MEMBER, () => {
|
||||
if (
|
||||
document.visibility !== DocumentVisibility.EVERYONE ||
|
||||
(data.visibility && data.visibility !== DocumentVisibility.EVERYONE)
|
||||
) {
|
||||
throw new AppError(
|
||||
AppErrorCode.UNAUTHORIZED,
|
||||
'You do not have permission to update the document visibility',
|
||||
);
|
||||
}
|
||||
})
|
||||
.otherwise(() => {
|
||||
throw new AppError(
|
||||
AppErrorCode.UNAUTHORIZED,
|
||||
'You do not have permission to update the document',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
});
|
||||
|
||||
@ -2,13 +2,15 @@ import { DateTime } from 'luxon';
|
||||
import type { Browser } from 'playwright';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
import { encryptSecondaryData } from '../crypto/encrypt';
|
||||
|
||||
export type GetCertificatePdfOptions = {
|
||||
documentId: number;
|
||||
language?: SupportedLanguageCodes;
|
||||
};
|
||||
|
||||
export const getCertificatePdf = async ({ documentId }: GetCertificatePdfOptions) => {
|
||||
export const getCertificatePdf = async ({ documentId, language }: GetCertificatePdfOptions) => {
|
||||
const { chromium } = await import('playwright');
|
||||
|
||||
const encryptedId = encryptSecondaryData({
|
||||
@ -32,7 +34,19 @@ export const getCertificatePdf = async ({ documentId }: GetCertificatePdfOptions
|
||||
);
|
||||
}
|
||||
|
||||
const page = await browser.newPage();
|
||||
const browserContext = await browser.newContext();
|
||||
|
||||
const page = await browserContext.newPage();
|
||||
|
||||
if (language) {
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: 'language',
|
||||
value: language,
|
||||
url: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encryptedId}`, {
|
||||
waitUntil: 'networkidle',
|
||||
@ -43,6 +57,8 @@ export const getCertificatePdf = async ({ documentId }: GetCertificatePdfOptions
|
||||
format: 'A4',
|
||||
});
|
||||
|
||||
await browserContext.close();
|
||||
|
||||
void browser.close();
|
||||
|
||||
return result;
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import {
|
||||
@ -21,10 +22,14 @@ import type { Recipient } from '@documenso/prisma/client';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
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';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
|
||||
export interface SetRecipientsForDocumentOptions {
|
||||
userId: number;
|
||||
@ -62,6 +67,12 @@ export const setRecipientsForDocument = async ({
|
||||
},
|
||||
include: {
|
||||
Field: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -276,10 +287,14 @@ export const setRecipientsForDocument = async ({
|
||||
});
|
||||
});
|
||||
|
||||
const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).recipientRemoved;
|
||||
|
||||
// Send emails to deleted recipients.
|
||||
await Promise.all(
|
||||
removedRecipients.map(async (recipient) => {
|
||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||
if (recipient.sendStatus !== SendStatus.SENT || !isRecipientRemovedEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -291,6 +306,17 @@ export const setRecipientsForDocument = async ({
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language }),
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: recipient.email,
|
||||
@ -300,9 +326,9 @@ export const setRecipientsForDocument = async ({
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: 'You have been removed from a document',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
subject: i18n._(msg`You have been removed from a document`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
@ -42,7 +42,16 @@ export const acceptTeamInvitation = async ({ userId, teamId }: AcceptTeamInvitat
|
||||
});
|
||||
|
||||
if (teamMemberInvite.status === TeamMemberInviteStatus.ACCEPTED) {
|
||||
return;
|
||||
const memberExists = await tx.teamMember.findFirst({
|
||||
where: {
|
||||
teamId: teamMemberInvite.teamId,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (memberExists) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { team } = teamMemberInvite;
|
||||
@ -81,7 +90,7 @@ export const acceptTeamInvitation = async ({ userId, teamId }: AcceptTeamInvitat
|
||||
await jobs.triggerJob({
|
||||
name: 'send.team-member-joined.email',
|
||||
payload: {
|
||||
teamId: team.id,
|
||||
teamId: teamMember.teamId,
|
||||
memberId: teamMember.id,
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import { ConfirmTeamEmailTemplate } from '@documenso/email/templates/confirm-team-email';
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
@ -11,8 +11,13 @@ 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 type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
|
||||
import { Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
|
||||
export type CreateTeamEmailVerificationOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
@ -45,6 +50,7 @@ export const createTeamEmailVerification = async ({
|
||||
include: {
|
||||
teamEmail: true,
|
||||
emailVerification: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -77,7 +83,7 @@ export const createTeamEmailVerification = async ({
|
||||
},
|
||||
});
|
||||
|
||||
await sendTeamEmailVerificationEmail(data.email, token, team.name, team.url);
|
||||
await sendTeamEmailVerificationEmail(data.email, token, team);
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
@ -109,27 +115,47 @@ export const createTeamEmailVerification = async ({
|
||||
export const sendTeamEmailVerificationEmail = async (
|
||||
email: string,
|
||||
token: string,
|
||||
teamName: string,
|
||||
teamUrl: string,
|
||||
team: Team & {
|
||||
teamGlobalSettings?: TeamGlobalSettings | null;
|
||||
},
|
||||
) => {
|
||||
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(ConfirmTeamEmailTemplate, {
|
||||
assetBaseUrl,
|
||||
baseUrl: WEBAPP_BASE_URL,
|
||||
teamName,
|
||||
teamUrl,
|
||||
teamName: team.name,
|
||||
teamUrl: team.url,
|
||||
token,
|
||||
});
|
||||
|
||||
const branding = team.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const lang = team.teamGlobalSettings?.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: `A request to use your email has been initiated by ${teamName} on Documenso`,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
subject: i18n._(
|
||||
msg`A request to use your email has been initiated by ${team.name} on Documenso`,
|
||||
),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import type { TeamInviteEmailProps } from '@documenso/email/templates/team-invite';
|
||||
import { TeamInviteEmailTemplate } from '@documenso/email/templates/team-invite';
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
@ -12,9 +11,14 @@ import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
|
||||
import { TeamMemberInviteStatus } from '@documenso/prisma/client';
|
||||
import type { TCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
|
||||
export type CreateTeamMemberInvitesOptions = {
|
||||
userId: number;
|
||||
userName: string;
|
||||
@ -56,6 +60,7 @@ export const createTeamMemberInvites = async ({
|
||||
},
|
||||
},
|
||||
invites: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -109,8 +114,7 @@ export const createTeamMemberInvites = async ({
|
||||
sendTeamMemberInviteEmail({
|
||||
email,
|
||||
token,
|
||||
teamName: team.name,
|
||||
teamUrl: team.url,
|
||||
team,
|
||||
senderName: userName,
|
||||
}),
|
||||
),
|
||||
@ -131,8 +135,13 @@ export const createTeamMemberInvites = async ({
|
||||
}
|
||||
};
|
||||
|
||||
type SendTeamMemberInviteEmailOptions = Omit<TeamInviteEmailProps, 'baseUrl' | 'assetBaseUrl'> & {
|
||||
type SendTeamMemberInviteEmailOptions = {
|
||||
email: string;
|
||||
senderName: string;
|
||||
token: string;
|
||||
team: Team & {
|
||||
teamGlobalSettings?: TeamGlobalSettings | null;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@ -140,22 +149,42 @@ type SendTeamMemberInviteEmailOptions = Omit<TeamInviteEmailProps, 'baseUrl' | '
|
||||
*/
|
||||
export const sendTeamMemberInviteEmail = async ({
|
||||
email,
|
||||
...emailTemplateOptions
|
||||
senderName,
|
||||
token,
|
||||
team,
|
||||
}: SendTeamMemberInviteEmailOptions) => {
|
||||
const template = createElement(TeamInviteEmailTemplate, {
|
||||
assetBaseUrl: WEBAPP_BASE_URL,
|
||||
baseUrl: WEBAPP_BASE_URL,
|
||||
...emailTemplateOptions,
|
||||
senderName,
|
||||
token,
|
||||
teamName: team.name,
|
||||
teamUrl: team.url,
|
||||
});
|
||||
|
||||
const branding = team.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: team.teamGlobalSettings?.documentLanguage, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: team.teamGlobalSettings?.documentLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(team.teamGlobalSettings?.documentLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: `You have been invited to join ${emailTemplateOptions.teamName} on Documenso`,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
subject: i18n._(msg`You have been invited to join ${team.name} on Documenso`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import { TeamEmailRemovedTemplate } from '@documenso/email/templates/team-email-removed';
|
||||
import { WEBAPP_BASE_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';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
|
||||
export type DeleteTeamEmailOptions = {
|
||||
userId: number;
|
||||
userEmail: string;
|
||||
@ -50,6 +55,7 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -73,6 +79,19 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE
|
||||
teamUrl: team.url,
|
||||
});
|
||||
|
||||
const branding = team.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const lang = team.teamGlobalSettings?.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding, plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: team.owner.email,
|
||||
@ -82,9 +101,9 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: `Team email has been revoked for ${team.name}`,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
subject: i18n._(msg`Team email has been revoked for ${team.name}`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
} catch (e) {
|
||||
// Todo: Teams - Alert us.
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import type { TeamDeleteEmailProps } from '@documenso/email/templates/team-delete';
|
||||
import { TeamDeleteEmailTemplate } from '@documenso/email/templates/team-delete';
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { jobs } from '../../jobs/client';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
|
||||
export type DeleteTeamOptions = {
|
||||
userId: number;
|
||||
@ -38,6 +42,7 @@ export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -60,6 +65,7 @@ export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => {
|
||||
name: team.name,
|
||||
url: team.url,
|
||||
ownerUserId: team.ownerUserId,
|
||||
teamGlobalSettings: team.teamGlobalSettings,
|
||||
},
|
||||
members: team.members.map((member) => ({
|
||||
id: member.user.id,
|
||||
@ -80,29 +86,43 @@ export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => {
|
||||
);
|
||||
};
|
||||
|
||||
type SendTeamDeleteEmailOptions = Omit<TeamDeleteEmailProps, 'baseUrl' | 'assetBaseUrl'> & {
|
||||
type SendTeamDeleteEmailOptions = {
|
||||
email: string;
|
||||
teamName: string;
|
||||
team: Pick<Team, 'url' | 'name'> & {
|
||||
teamGlobalSettings?: TeamGlobalSettings | null;
|
||||
};
|
||||
isOwner: boolean;
|
||||
};
|
||||
|
||||
export const sendTeamDeleteEmail = async ({
|
||||
email,
|
||||
...emailTemplateOptions
|
||||
}: SendTeamDeleteEmailOptions) => {
|
||||
export const sendTeamDeleteEmail = async ({ email, isOwner, team }: SendTeamDeleteEmailOptions) => {
|
||||
const template = createElement(TeamDeleteEmailTemplate, {
|
||||
assetBaseUrl: WEBAPP_BASE_URL,
|
||||
baseUrl: WEBAPP_BASE_URL,
|
||||
...emailTemplateOptions,
|
||||
teamUrl: team.url,
|
||||
isOwner,
|
||||
});
|
||||
|
||||
const branding = team.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const lang = team.teamGlobalSettings?.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding, plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: `Team "${emailTemplateOptions.teamName}" has been deleted on Documenso`,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
subject: i18n._(msg`Team "${team.name}" has been deleted on Documenso`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
|
||||
@ -30,6 +30,7 @@ export const getTeamById = async ({ userId, teamId }: GetTeamByIdOptions) => {
|
||||
where: whereFilter,
|
||||
include: {
|
||||
teamEmail: true,
|
||||
teamGlobalSettings: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
@ -89,6 +90,7 @@ export const getTeamByUrl = async ({ userId, teamUrl }: GetTeamByUrlOptions) =>
|
||||
},
|
||||
},
|
||||
subscription: true,
|
||||
teamGlobalSettings: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import { TeamTransferRequestTemplate } from '@documenso/email/templates/team-transfer-request';
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import { createTokenVerification } from '@documenso/lib/utils/token-verification';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
|
||||
export type RequestTeamOwnershipTransferOptions = {
|
||||
/**
|
||||
* The ID of the user initiating the transfer.
|
||||
@ -93,15 +97,24 @@ export const requestTeamOwnershipTransfer = async ({
|
||||
token,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template),
|
||||
renderEmailWithI18N(template, { plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance();
|
||||
|
||||
await mailer.sendMail({
|
||||
to: newOwnerUser.email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: `You have been requested to take ownership of team ${team.name} on Documenso`,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
subject: i18n._(
|
||||
msg`You have been requested to take ownership of team ${team.name} on Documenso`,
|
||||
),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
|
||||
@ -33,6 +33,7 @@ export const resendTeamEmailVerification = async ({
|
||||
},
|
||||
include: {
|
||||
emailVerification: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -61,7 +62,7 @@ export const resendTeamEmailVerification = async ({
|
||||
},
|
||||
});
|
||||
|
||||
await sendTeamEmailVerificationEmail(emailVerification.email, token, team.name, team.url);
|
||||
await sendTeamEmailVerificationEmail(emailVerification.email, token, team);
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
|
||||
@ -49,6 +49,9 @@ export const resendTeamMemberInvitation = async ({
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
@ -69,9 +72,8 @@ export const resendTeamMemberInvitation = async ({
|
||||
await sendTeamMemberInviteEmail({
|
||||
email: teamMemberInvite.email,
|
||||
token: teamMemberInvite.token,
|
||||
teamName: team.name,
|
||||
teamUrl: team.url,
|
||||
senderName: userName,
|
||||
team,
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
export type UpdateTeamBrandingSettingsOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
|
||||
settings: {
|
||||
brandingEnabled: boolean;
|
||||
brandingLogo: string;
|
||||
brandingUrl: string;
|
||||
brandingCompanyDetails: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const updateTeamBrandingSettings = async ({
|
||||
userId,
|
||||
teamId,
|
||||
settings,
|
||||
}: UpdateTeamBrandingSettingsOptions) => {
|
||||
const { brandingEnabled, brandingLogo, brandingUrl, brandingCompanyDetails } = settings;
|
||||
|
||||
const member = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!member || member.role !== TeamMemberRole.ADMIN) {
|
||||
throw new Error('You do not have permission to update this team.');
|
||||
}
|
||||
|
||||
return await prisma.teamGlobalSettings.upsert({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
create: {
|
||||
teamId,
|
||||
brandingEnabled,
|
||||
brandingLogo,
|
||||
brandingUrl,
|
||||
brandingCompanyDetails,
|
||||
},
|
||||
update: {
|
||||
brandingEnabled,
|
||||
brandingLogo,
|
||||
brandingUrl,
|
||||
brandingCompanyDetails,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,52 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
|
||||
export type UpdateTeamDocumentSettingsOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
|
||||
settings: {
|
||||
documentVisibility: DocumentVisibility;
|
||||
documentLanguage: SupportedLanguageCodes;
|
||||
includeSenderDetails: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const updateTeamDocumentSettings = async ({
|
||||
userId,
|
||||
teamId,
|
||||
settings,
|
||||
}: UpdateTeamDocumentSettingsOptions) => {
|
||||
const { documentVisibility, documentLanguage, includeSenderDetails } = settings;
|
||||
|
||||
const member = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!member || member.role !== TeamMemberRole.ADMIN) {
|
||||
throw new Error('You do not have permission to update this team.');
|
||||
}
|
||||
|
||||
return await prisma.teamGlobalSettings.upsert({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
create: {
|
||||
teamId,
|
||||
documentVisibility,
|
||||
documentLanguage,
|
||||
includeSenderDetails,
|
||||
},
|
||||
update: {
|
||||
documentVisibility,
|
||||
documentLanguage,
|
||||
includeSenderDetails,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -4,6 +4,7 @@ import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { Prisma } from '@documenso/prisma/client';
|
||||
import type { DocumentVisibility } from '@documenso/prisma/client';
|
||||
|
||||
export type UpdateTeamOptions = {
|
||||
userId: number;
|
||||
@ -11,6 +12,8 @@ export type UpdateTeamOptions = {
|
||||
data: {
|
||||
name?: string;
|
||||
url?: string;
|
||||
documentVisibility?: DocumentVisibility;
|
||||
includeSenderDetails?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@ -42,6 +45,18 @@ export const updateTeam = async ({ userId, teamId, data }: UpdateTeamOptions) =>
|
||||
data: {
|
||||
url: data.url,
|
||||
name: data.name,
|
||||
teamGlobalSettings: {
|
||||
upsert: {
|
||||
create: {
|
||||
documentVisibility: data.documentVisibility,
|
||||
includeSenderDetails: data.includeSenderDetails,
|
||||
},
|
||||
update: {
|
||||
documentVisibility: data.documentVisibility,
|
||||
includeSenderDetails: data.includeSenderDetails,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import { DocumentCreatedFromDirectTemplateEmailTemplate } from '@documenso/email/templates/document-created-from-direct-template';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Field, Signature } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
FieldType,
|
||||
@ -21,6 +22,7 @@ import {
|
||||
} from '@documenso/prisma/client';
|
||||
import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/field-router/schema';
|
||||
|
||||
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';
|
||||
@ -37,6 +39,8 @@ import {
|
||||
createRecipientAuthOptions,
|
||||
extractDocumentAuthMethods,
|
||||
} from '../../utils/document-auth';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { formatDocumentsPath } from '../../utils/teams';
|
||||
import { sendDocument } from '../document/send-document';
|
||||
import { validateFieldAuth } from '../document/validate-field-auth';
|
||||
@ -88,6 +92,11 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
User: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -142,6 +151,9 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
const metaDateFormat = template.templateMeta?.dateFormat || DEFAULT_DOCUMENT_DATE_FORMAT;
|
||||
const metaEmailMessage = template.templateMeta?.message || '';
|
||||
const metaEmailSubject = template.templateMeta?.subject || '';
|
||||
const metaLanguage =
|
||||
template.templateMeta?.language ?? template.team?.teamGlobalSettings?.documentLanguage;
|
||||
const metaSigningOrder = template.templateMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
|
||||
|
||||
// Associate, validate and map to a query every direct template recipient field with the provided fields.
|
||||
const createDirectRecipientFieldArgs = await Promise.all(
|
||||
@ -232,6 +244,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
createdAt: initialRequestTime,
|
||||
status: DocumentStatus.PENDING,
|
||||
externalId: directTemplateExternalId,
|
||||
visibility: template.team?.teamGlobalSettings?.documentVisibility,
|
||||
documentDataId: documentData.id,
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: templateAuthOptions.globalAccessAuth,
|
||||
@ -256,6 +269,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
recipient.role === RecipientRole.CC
|
||||
? SigningStatus.SIGNED
|
||||
: SigningStatus.NOT_SIGNED,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
};
|
||||
}),
|
||||
@ -267,6 +281,9 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
dateFormat: metaDateFormat,
|
||||
message: metaEmailMessage,
|
||||
subject: metaEmailSubject,
|
||||
language: metaLanguage,
|
||||
signingOrder: metaSigningOrder,
|
||||
distributionMethod: template.templateMeta?.distributionMethod,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -330,6 +347,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
signedAt: initialRequestTime,
|
||||
signingOrder: directTemplateRecipient.signingOrder,
|
||||
Field: {
|
||||
createMany: {
|
||||
data: directTemplateNonSignatureFields.map(({ templateField, customText }) => ({
|
||||
@ -524,6 +542,17 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
|
||||
});
|
||||
|
||||
const branding = template.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(template.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailTemplate, { lang: metaLanguage, branding }),
|
||||
renderEmailWithI18N(emailTemplate, { lang: metaLanguage, branding, plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(metaLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: [
|
||||
{
|
||||
@ -535,9 +564,9 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||
},
|
||||
subject: 'Document created from direct template',
|
||||
html: render(emailTemplate),
|
||||
text: render(emailTemplate, { plainText: true }),
|
||||
subject: i18n._(msg`Document created from direct template`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@ -46,6 +46,12 @@ export const createDocumentFromTemplateLegacy = async ({
|
||||
Recipient: true,
|
||||
Field: true,
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -68,6 +74,7 @@ export const createDocumentFromTemplateLegacy = async ({
|
||||
userId,
|
||||
teamId: template.teamId,
|
||||
title: template.title,
|
||||
visibility: template.team?.teamGlobalSettings?.documentVisibility,
|
||||
documentDataId: documentData.id,
|
||||
Recipient: {
|
||||
create: template.Recipient.map((recipient) => ({
|
||||
@ -78,6 +85,18 @@ export const createDocumentFromTemplateLegacy = async ({
|
||||
token: nanoid(),
|
||||
})),
|
||||
},
|
||||
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 || template.team?.teamGlobalSettings?.documentLanguage,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
include: {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentDistributionMethod } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentSource,
|
||||
@ -11,6 +12,7 @@ import {
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { ZRecipientAuthOptionsSchema } from '../../types/document-auth';
|
||||
@ -24,7 +26,10 @@ import {
|
||||
} from '../../utils/document-auth';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
type FinalRecipient = Pick<Recipient, 'name' | 'email' | 'role' | 'authOptions'> & {
|
||||
type FinalRecipient = Pick<
|
||||
Recipient,
|
||||
'name' | 'email' | 'role' | 'authOptions' | 'signingOrder'
|
||||
> & {
|
||||
templateRecipientId: number;
|
||||
fields: Field[];
|
||||
};
|
||||
@ -57,6 +62,8 @@ export type CreateDocumentFromTemplateOptions = {
|
||||
dateFormat?: string;
|
||||
redirectUrl?: string;
|
||||
signingOrder?: DocumentSigningOrder;
|
||||
language?: SupportedLanguageCodes;
|
||||
distributionMethod?: DocumentDistributionMethod;
|
||||
};
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
@ -103,6 +110,11 @@ export const createDocumentFromTemplate = async ({
|
||||
},
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -164,6 +176,7 @@ export const createDocumentFromTemplate = async ({
|
||||
globalAccessAuth: templateAuthOptions.globalAccessAuth,
|
||||
globalActionAuth: templateAuthOptions.globalActionAuth,
|
||||
}),
|
||||
visibility: template.team?.teamGlobalSettings?.documentVisibility,
|
||||
documentMeta: {
|
||||
create: {
|
||||
subject: override?.subject || template.templateMeta?.subject,
|
||||
@ -172,10 +185,17 @@ export const createDocumentFromTemplate = async ({
|
||||
password: override?.password || template.templateMeta?.password,
|
||||
dateFormat: override?.dateFormat || template.templateMeta?.dateFormat,
|
||||
redirectUrl: override?.redirectUrl || template.templateMeta?.redirectUrl,
|
||||
distributionMethod:
|
||||
override?.distributionMethod || template.templateMeta?.distributionMethod,
|
||||
emailSettings: template.templateMeta?.emailSettings || undefined,
|
||||
signingOrder:
|
||||
override?.signingOrder ||
|
||||
template.templateMeta?.signingOrder ||
|
||||
DocumentSigningOrder.PARALLEL,
|
||||
language:
|
||||
override?.language ||
|
||||
template.templateMeta?.language ||
|
||||
template.team?.teamGlobalSettings?.documentLanguage,
|
||||
},
|
||||
},
|
||||
Recipient: {
|
||||
@ -197,6 +217,7 @@ export const createDocumentFromTemplate = async ({
|
||||
recipient.role === RecipientRole.CC
|
||||
? SigningStatus.SIGNED
|
||||
: SigningStatus.NOT_SIGNED,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
};
|
||||
}),
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { omit } from 'remeda';
|
||||
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
@ -38,6 +40,7 @@ export const duplicateTemplate = async ({
|
||||
Recipient: true,
|
||||
Field: true,
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -53,6 +56,17 @@ export const duplicateTemplate = async ({
|
||||
},
|
||||
});
|
||||
|
||||
let templateMeta: Prisma.TemplateCreateArgs['data']['templateMeta'] | undefined = undefined;
|
||||
|
||||
if (template.templateMeta) {
|
||||
templateMeta = {
|
||||
create: {
|
||||
...omit(template.templateMeta, ['id', 'templateId']),
|
||||
emailSettings: template.templateMeta.emailSettings || undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const duplicatedTemplate = await prisma.template.create({
|
||||
data: {
|
||||
userId,
|
||||
@ -66,8 +80,8 @@ export const duplicateTemplate = async ({
|
||||
token: nanoid(),
|
||||
})),
|
||||
},
|
||||
templateMeta,
|
||||
},
|
||||
|
||||
include: {
|
||||
Recipient: true,
|
||||
},
|
||||
|
||||
@ -54,6 +54,7 @@ export const findTemplates = async ({
|
||||
templateMeta: {
|
||||
select: {
|
||||
signingOrder: true,
|
||||
distributionMethod: true,
|
||||
},
|
||||
},
|
||||
directLink: {
|
||||
|
||||
@ -42,6 +42,13 @@ export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOpt
|
||||
templateMeta: true,
|
||||
Recipient: true,
|
||||
Field: true,
|
||||
User: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -112,9 +112,11 @@ export const updateTemplateSettings = async ({
|
||||
},
|
||||
create: {
|
||||
...meta,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
},
|
||||
update: {
|
||||
...meta,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -3,7 +3,7 @@ import { hash } from '@node-rs/bcrypt';
|
||||
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
|
||||
import { updateSubscriptionItemQuantity } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { IdentityProvider, Prisma, TeamMemberInviteStatus } from '@documenso/prisma/client';
|
||||
import { IdentityProvider, TeamMemberInviteStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '../../constants/app';
|
||||
import { SALT_ROUNDS } from '../../constants/auth';
|
||||
@ -59,11 +59,11 @@ export const createUser = async ({ name, email, password, signature, url }: Crea
|
||||
|
||||
const acceptedTeamInvites = await prisma.teamMemberInvite.findMany({
|
||||
where: {
|
||||
status: TeamMemberInviteStatus.ACCEPTED,
|
||||
email: {
|
||||
equals: email,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
status: TeamMemberInviteStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ export const sendConfirmationToken = async ({
|
||||
mostRecentToken?.createdAt &&
|
||||
DateTime.fromJSDate(mostRecentToken.createdAt).diffNow('minutes').minutes > -5
|
||||
) {
|
||||
return;
|
||||
// return;
|
||||
}
|
||||
|
||||
const createdToken = await prisma.verificationToken.create({
|
||||
@ -64,6 +64,7 @@ export const sendConfirmationToken = async ({
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw new Error(`Failed to send the confirmation email`);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user