mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
## Description Support setting a document language that will control the language used for sending emails to recipients. Additional work has been done to convert all emails to using our i18n implementation so we can later add controls for sending other kinds of emails in a users target language. ## Related Issue N/A ## Changes Made - Added `<Trans>` and `msg` macros to emails - Introduced a new `renderEmailWithI18N` utility in the lib package - Updated all emails to use the `<Tailwind>` component at the top level due to rendering constraints - Updated the `i18n.server.tsx` file to not use a top level await ## Testing Performed - Configured document language and verified emails were sent in the expected language - Created a document from a template and verified that the templates language was transferred to the document
148 lines
4.0 KiB
TypeScript
148 lines
4.0 KiB
TypeScript
import { createElement } from 'react';
|
|
|
|
import { msg } from '@lingui/macro';
|
|
import { z } from 'zod';
|
|
|
|
import { mailer } from '@documenso/email/mailer';
|
|
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';
|
|
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 { Prisma } from '@documenso/prisma/client';
|
|
|
|
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
|
|
|
export type CreateTeamEmailVerificationOptions = {
|
|
userId: number;
|
|
teamId: number;
|
|
data: {
|
|
email: string;
|
|
name: string;
|
|
};
|
|
};
|
|
|
|
export const createTeamEmailVerification = async ({
|
|
userId,
|
|
teamId,
|
|
data,
|
|
}: CreateTeamEmailVerificationOptions) => {
|
|
try {
|
|
await prisma.$transaction(
|
|
async (tx) => {
|
|
const team = await tx.team.findFirstOrThrow({
|
|
where: {
|
|
id: teamId,
|
|
members: {
|
|
some: {
|
|
userId,
|
|
role: {
|
|
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
include: {
|
|
teamEmail: true,
|
|
emailVerification: true,
|
|
},
|
|
});
|
|
|
|
if (team.teamEmail || team.emailVerification) {
|
|
throw new AppError(
|
|
AppErrorCode.INVALID_REQUEST,
|
|
'Team already has an email or existing email verification.',
|
|
);
|
|
}
|
|
|
|
const existingTeamEmail = await tx.teamEmail.findFirst({
|
|
where: {
|
|
email: data.email,
|
|
},
|
|
});
|
|
|
|
if (existingTeamEmail) {
|
|
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Email already taken by another team.');
|
|
}
|
|
|
|
const { token, expiresAt } = createTokenVerification({ hours: 1 });
|
|
|
|
await tx.teamEmailVerification.create({
|
|
data: {
|
|
token,
|
|
expiresAt,
|
|
email: data.email,
|
|
name: data.name,
|
|
teamId,
|
|
},
|
|
});
|
|
|
|
await sendTeamEmailVerificationEmail(data.email, token, team.name, team.url);
|
|
},
|
|
{ timeout: 30_000 },
|
|
);
|
|
} catch (err) {
|
|
console.error(err);
|
|
|
|
if (!(err instanceof Prisma.PrismaClientKnownRequestError)) {
|
|
throw err;
|
|
}
|
|
|
|
const target = z.array(z.string()).safeParse(err.meta?.target);
|
|
|
|
if (err.code === 'P2002' && target.success && target.data.includes('email')) {
|
|
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Email already taken by another team.');
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Send an email to a user asking them to accept a team email request.
|
|
*
|
|
* @param email The email address to use for the team.
|
|
* @param token The token used to authenticate that the user has granted access.
|
|
* @param teamName The name of the team the user is being invited to.
|
|
* @param teamUrl The url of the team the user is being invited to.
|
|
*/
|
|
export const sendTeamEmailVerificationEmail = async (
|
|
email: string,
|
|
token: string,
|
|
teamName: string,
|
|
teamUrl: string,
|
|
) => {
|
|
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
|
|
|
|
const template = createElement(ConfirmTeamEmailTemplate, {
|
|
assetBaseUrl,
|
|
baseUrl: WEBAPP_BASE_URL,
|
|
teamName,
|
|
teamUrl,
|
|
token,
|
|
});
|
|
|
|
const [html, text] = await Promise.all([
|
|
renderEmailWithI18N(template),
|
|
renderEmailWithI18N(template, { plainText: true }),
|
|
]);
|
|
|
|
const i18n = await getI18nInstance();
|
|
|
|
await mailer.sendMail({
|
|
to: email,
|
|
from: {
|
|
name: FROM_NAME,
|
|
address: FROM_ADDRESS,
|
|
},
|
|
subject: i18n._(
|
|
msg`A request to use your email has been initiated by ${teamName} on Documenso`,
|
|
),
|
|
html,
|
|
text,
|
|
});
|
|
};
|