mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
Emails, the signing page's title, the sealed PDF's reason, the sign-in refusal and the dialogs still said Hanzo Sign. The catalogs change with the strings, message for message, so every translation keeps its id.
154 lines
4.3 KiB
TypeScript
154 lines
4.3 KiB
TypeScript
import { createElement } from 'react';
|
|
|
|
import { msg } from '@lingui/core/macro';
|
|
import type { Team } from '@prisma/client';
|
|
import { Prisma } from '@prisma/client';
|
|
import { z } from 'zod';
|
|
|
|
import { mailer } from '@hanzo/esign-email/mailer';
|
|
import { ConfirmTeamEmailTemplate } from '@hanzo/esign-email/templates/confirm-team-email';
|
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@hanzo/esign-lib/constants/app';
|
|
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@hanzo/esign-lib/constants/teams';
|
|
import { AppError, AppErrorCode } from '@hanzo/esign-lib/errors/app-error';
|
|
import { createTokenVerification } from '@hanzo/esign-lib/utils/token-verification';
|
|
import { prisma } from '@hanzo/esign-prisma';
|
|
|
|
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
|
import { env } from '../../utils/env';
|
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
|
import { buildTeamWhereQuery } from '../../utils/teams';
|
|
import { getEmailContext } from '../email/get-email-context';
|
|
|
|
export type CreateTeamEmailVerificationOptions = {
|
|
userId: number;
|
|
teamId: number;
|
|
data: {
|
|
email: string;
|
|
name: string;
|
|
};
|
|
};
|
|
|
|
export const createTeamEmailVerification = async ({
|
|
userId,
|
|
teamId,
|
|
data,
|
|
}: CreateTeamEmailVerificationOptions): Promise<void> => {
|
|
try {
|
|
const team = await prisma.team.findFirstOrThrow({
|
|
where: buildTeamWhereQuery({
|
|
teamId,
|
|
userId,
|
|
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
|
}),
|
|
include: {
|
|
teamEmail: true,
|
|
emailVerification: true,
|
|
},
|
|
});
|
|
|
|
if (team.teamEmail || team.emailVerification) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: 'Team already has an email or existing email verification.',
|
|
});
|
|
}
|
|
|
|
const { token } = await prisma.$transaction(
|
|
async (tx) => {
|
|
const existingTeamEmail = await tx.teamEmail.findFirst({
|
|
where: {
|
|
email: data.email,
|
|
},
|
|
});
|
|
|
|
if (existingTeamEmail) {
|
|
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
|
|
message: '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,
|
|
},
|
|
});
|
|
|
|
return { token };
|
|
},
|
|
{ timeout: 30_000 },
|
|
);
|
|
|
|
await sendTeamEmailVerificationEmail(data.email, token, team);
|
|
} 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, {
|
|
message: '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, team: Team) => {
|
|
const assetBaseUrl = env('NEXT_PUBLIC_WEBAPP_URL') || 'http://localhost:3000';
|
|
|
|
const template = createElement(ConfirmTeamEmailTemplate, {
|
|
assetBaseUrl,
|
|
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
|
teamName: team.name,
|
|
teamUrl: team.url,
|
|
token,
|
|
});
|
|
|
|
const { branding, emailLanguage, senderEmail } = await getEmailContext({
|
|
emailType: 'INTERNAL',
|
|
source: {
|
|
type: 'team',
|
|
teamId: team.id,
|
|
},
|
|
});
|
|
|
|
const [html, text] = await Promise.all([
|
|
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
|
renderEmailWithI18N(template, {
|
|
lang: emailLanguage,
|
|
branding,
|
|
plainText: true,
|
|
}),
|
|
]);
|
|
|
|
const i18n = await getI18nInstance(emailLanguage);
|
|
|
|
await mailer.sendMail({
|
|
to: email,
|
|
from: senderEmail,
|
|
subject: i18n._(
|
|
msg`A request to use your email has been initiated by ${team.name} on Hanzo eSign`,
|
|
),
|
|
html,
|
|
text,
|
|
});
|
|
};
|