mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-30 14:57:59 +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.
197 lines
5.9 KiB
TypeScript
197 lines
5.9 KiB
TypeScript
import { createElement } from 'react';
|
|
|
|
import { msg } from '@lingui/core/macro';
|
|
import type { Organisation, Prisma } from '@prisma/client';
|
|
import { OrganisationMemberInviteStatus } from '@prisma/client';
|
|
|
|
import { mailer } from '@hanzo/esign-email/mailer';
|
|
import { OrganisationInviteEmailTemplate } from '@hanzo/esign-email/templates/organisation-invite';
|
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@hanzo/esign-lib/constants/app';
|
|
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@hanzo/esign-lib/constants/organisations';
|
|
import { AppError, AppErrorCode } from '@hanzo/esign-lib/errors/app-error';
|
|
import { isOrganisationRoleWithinUserHierarchy } from '@hanzo/esign-lib/utils/organisations';
|
|
import { prisma } from '@hanzo/esign-prisma';
|
|
import type { TCreateOrganisationMemberInvitesRequestSchema } from '@hanzo/esign-trpc/server/organisation-router/create-organisation-member-invites.types';
|
|
|
|
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
|
import { buildOrganisationWhereQuery } from '../../utils/organisations';
|
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
|
import { getEmailContext } from '../email/get-email-context';
|
|
import { getMemberOrganisationRole } from '../team/get-member-roles';
|
|
import { assertAdmin } from '../user/assert-admin';
|
|
import { newInvitation } from './new-invitation';
|
|
|
|
export type CreateOrganisationMemberInvitesOptions = {
|
|
userId: number;
|
|
userName: string;
|
|
organisationId: string;
|
|
invitations: TCreateOrganisationMemberInvitesRequestSchema['invitations'];
|
|
};
|
|
|
|
/**
|
|
* Invite organisation members via email to join a organisation. Only an ADMIN
|
|
* invites, and the invitation records who sent it.
|
|
*/
|
|
export const createOrganisationMemberInvites = async ({
|
|
userId,
|
|
userName,
|
|
organisationId,
|
|
invitations,
|
|
}: CreateOrganisationMemberInvitesOptions): Promise<void> => {
|
|
await assertAdmin(userId);
|
|
|
|
const organisation = await prisma.organisation.findFirst({
|
|
where: buildOrganisationWhereQuery({
|
|
organisationId,
|
|
userId,
|
|
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
|
}),
|
|
include: {
|
|
members: {
|
|
select: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
invites: {
|
|
where: {
|
|
status: OrganisationMemberInviteStatus.PENDING,
|
|
},
|
|
},
|
|
organisationGlobalSettings: true,
|
|
},
|
|
});
|
|
|
|
if (!organisation) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND);
|
|
}
|
|
|
|
const currentOrganisationMemberRole = await getMemberOrganisationRole({
|
|
organisationId: organisation.id,
|
|
reference: {
|
|
type: 'User',
|
|
id: userId,
|
|
},
|
|
});
|
|
|
|
const organisationMemberEmails = organisation.members.map((member) => member.user.email);
|
|
const organisationMemberInviteEmails = organisation.invites.map((invite) => invite.email);
|
|
|
|
const usersToInvite = invitations.filter((invitation) => {
|
|
// Filter out users that are already members of the organisation.
|
|
if (organisationMemberEmails.includes(invitation.email)) {
|
|
return false;
|
|
}
|
|
|
|
// Filter out users that have already been invited to the organisation.
|
|
if (organisationMemberInviteEmails.includes(invitation.email)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
const unauthorizedRoleAccess = usersToInvite.some(
|
|
({ organisationRole }) =>
|
|
!isOrganisationRoleWithinUserHierarchy(currentOrganisationMemberRole, organisationRole),
|
|
);
|
|
|
|
if (unauthorizedRoleAccess) {
|
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
message: 'User does not have permission to set high level roles',
|
|
});
|
|
}
|
|
|
|
const organisationMemberInvites: Prisma.OrganisationMemberInviteCreateManyInput[] =
|
|
usersToInvite.map(({ email, organisationRole }) =>
|
|
newInvitation({ email, organisationId, organisationRole }, userId),
|
|
);
|
|
|
|
await prisma.organisationMemberInvite.createMany({
|
|
data: organisationMemberInvites,
|
|
});
|
|
|
|
const sendEmailResult = await Promise.allSettled(
|
|
organisationMemberInvites.map(async ({ email, token }) =>
|
|
sendOrganisationMemberInviteEmail({
|
|
email,
|
|
token,
|
|
organisation,
|
|
senderName: userName,
|
|
}),
|
|
),
|
|
);
|
|
|
|
const sendEmailResultErrorList = sendEmailResult.filter(
|
|
(result): result is PromiseRejectedResult => result.status === 'rejected',
|
|
);
|
|
|
|
if (sendEmailResultErrorList.length > 0) {
|
|
console.error(JSON.stringify(sendEmailResultErrorList));
|
|
|
|
throw new AppError('EmailDeliveryFailed', {
|
|
message: 'Failed to send invite emails to one or more users.',
|
|
userMessage: `Failed to send invites to ${sendEmailResultErrorList.length}/${organisationMemberInvites.length} users.`,
|
|
});
|
|
}
|
|
};
|
|
|
|
type SendOrganisationMemberInviteEmailOptions = {
|
|
email: string;
|
|
senderName: string;
|
|
token: string;
|
|
organisation: Pick<Organisation, 'id' | 'name'>;
|
|
};
|
|
|
|
/**
|
|
* Send an email to a user inviting them to join a organisation.
|
|
*/
|
|
export const sendOrganisationMemberInviteEmail = async ({
|
|
email,
|
|
senderName,
|
|
token,
|
|
organisation,
|
|
}: SendOrganisationMemberInviteEmailOptions) => {
|
|
const template = createElement(OrganisationInviteEmailTemplate, {
|
|
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
|
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
|
senderName,
|
|
token,
|
|
organisationName: organisation.name,
|
|
});
|
|
|
|
const { branding, emailLanguage, senderEmail } = await getEmailContext({
|
|
emailType: 'INTERNAL',
|
|
source: {
|
|
type: 'organisation',
|
|
organisationId: organisation.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`You have been invited to join ${organisation.name} on Hanzo eSign`),
|
|
html,
|
|
text,
|
|
});
|
|
};
|