mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
fix: add timeouts to longer transactions
This commit is contained in:
@ -16,9 +16,8 @@ import { prisma } from '@documenso/prisma';
|
|||||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
import type { Prisma } from '@documenso/prisma/client';
|
import type { Prisma } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { getDocumentWhereInput } from './get-document-by-id';
|
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
|
import { getDocumentWhereInput } from './get-document-by-id';
|
||||||
|
|
||||||
export type ResendDocumentOptions = {
|
export type ResendDocumentOptions = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
@ -111,40 +110,43 @@ export const resendDocument = async ({
|
|||||||
|
|
||||||
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(
|
||||||
await mailer.sendMail({
|
async (tx) => {
|
||||||
to: {
|
await mailer.sendMail({
|
||||||
address: email,
|
to: {
|
||||||
name,
|
address: email,
|
||||||
},
|
name,
|
||||||
from: {
|
|
||||||
name: FROM_NAME,
|
|
||||||
address: FROM_ADDRESS,
|
|
||||||
},
|
|
||||||
subject: customEmail?.subject
|
|
||||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
|
||||||
: `Please ${actionVerb.toLowerCase()} this document`,
|
|
||||||
html: render(template),
|
|
||||||
text: render(template, { plainText: true }),
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
|
||||||
documentId: document.id,
|
|
||||||
user,
|
|
||||||
requestMetadata,
|
|
||||||
data: {
|
|
||||||
emailType: recipientEmailType,
|
|
||||||
recipientEmail: recipient.email,
|
|
||||||
recipientName: recipient.name,
|
|
||||||
recipientRole: recipient.role,
|
|
||||||
recipientId: recipient.id,
|
|
||||||
isResending: true,
|
|
||||||
},
|
},
|
||||||
}),
|
from: {
|
||||||
});
|
name: FROM_NAME,
|
||||||
});
|
address: FROM_ADDRESS,
|
||||||
|
},
|
||||||
|
subject: customEmail?.subject
|
||||||
|
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||||
|
: `Please ${actionVerb.toLowerCase()} this document`,
|
||||||
|
html: render(template),
|
||||||
|
text: render(template, { plainText: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||||
|
documentId: document.id,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
emailType: recipientEmailType,
|
||||||
|
recipientEmail: recipient.email,
|
||||||
|
recipientName: recipient.name,
|
||||||
|
recipientRole: recipient.role,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
isResending: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -49,44 +49,47 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
|||||||
downloadLink: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${token}/complete`,
|
downloadLink: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${token}/complete`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(
|
||||||
await mailer.sendMail({
|
async (tx) => {
|
||||||
to: {
|
await mailer.sendMail({
|
||||||
address: email,
|
to: {
|
||||||
name,
|
address: email,
|
||||||
},
|
name,
|
||||||
from: {
|
|
||||||
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 }),
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: document.title,
|
|
||||||
content: Buffer.from(buffer),
|
|
||||||
},
|
},
|
||||||
],
|
from: {
|
||||||
});
|
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 }),
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
filename: document.title,
|
||||||
|
content: Buffer.from(buffer),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
await tx.documentAuditLog.create({
|
await tx.documentAuditLog.create({
|
||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
user: null,
|
user: null,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
emailType: 'DOCUMENT_COMPLETED',
|
emailType: 'DOCUMENT_COMPLETED',
|
||||||
recipientEmail: recipient.email,
|
recipientEmail: recipient.email,
|
||||||
recipientName: recipient.name,
|
recipientName: recipient.name,
|
||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
recipientRole: recipient.role,
|
recipientRole: recipient.role,
|
||||||
isResending: false,
|
isResending: false,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -108,49 +108,52 @@ export const sendDocument = async ({
|
|||||||
|
|
||||||
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(
|
||||||
await mailer.sendMail({
|
async (tx) => {
|
||||||
to: {
|
await mailer.sendMail({
|
||||||
address: email,
|
to: {
|
||||||
name,
|
address: email,
|
||||||
},
|
name,
|
||||||
from: {
|
|
||||||
name: FROM_NAME,
|
|
||||||
address: FROM_ADDRESS,
|
|
||||||
},
|
|
||||||
subject: customEmail?.subject
|
|
||||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
|
||||||
: `Please ${actionVerb.toLowerCase()} this document`,
|
|
||||||
html: render(template),
|
|
||||||
text: render(template, { plainText: true }),
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.recipient.update({
|
|
||||||
where: {
|
|
||||||
id: recipient.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
sendStatus: SendStatus.SENT,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
|
||||||
documentId: document.id,
|
|
||||||
user,
|
|
||||||
requestMetadata,
|
|
||||||
data: {
|
|
||||||
emailType: recipientEmailType,
|
|
||||||
recipientEmail: recipient.email,
|
|
||||||
recipientName: recipient.name,
|
|
||||||
recipientRole: recipient.role,
|
|
||||||
recipientId: recipient.id,
|
|
||||||
isResending: false,
|
|
||||||
},
|
},
|
||||||
}),
|
from: {
|
||||||
});
|
name: FROM_NAME,
|
||||||
});
|
address: FROM_ADDRESS,
|
||||||
|
},
|
||||||
|
subject: customEmail?.subject
|
||||||
|
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||||
|
: `Please ${actionVerb.toLowerCase()} this document`,
|
||||||
|
html: render(template),
|
||||||
|
text: render(template, { plainText: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.recipient.update({
|
||||||
|
where: {
|
||||||
|
id: recipient.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
sendStatus: SendStatus.SENT,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||||
|
documentId: document.id,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
emailType: recipientEmailType,
|
||||||
|
recipientEmail: recipient.email,
|
||||||
|
recipientName: recipient.name,
|
||||||
|
recipientRole: recipient.role,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
isResending: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -24,34 +24,38 @@ export const updateTitle = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return await prisma.$transaction(async (tx) => {
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
const document = await tx.document.findFirstOrThrow({
|
where: {
|
||||||
where: {
|
id: documentId,
|
||||||
id: documentId,
|
OR: [
|
||||||
OR: [
|
{
|
||||||
{
|
userId,
|
||||||
userId,
|
},
|
||||||
},
|
{
|
||||||
{
|
team: {
|
||||||
team: {
|
members: {
|
||||||
members: {
|
some: {
|
||||||
some: {
|
userId,
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (document.title === title) {
|
if (document.title === title) {
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await prisma.$transaction(async (tx) => {
|
||||||
|
// Instead of doing everything in a transaction we can use our knowledge
|
||||||
|
// of the current document title to ensure we aren't performing a conflicting
|
||||||
|
// update.
|
||||||
const updatedDocument = await tx.document.update({
|
const updatedDocument = await tx.document.update({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
|
title: document.title,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
title,
|
title,
|
||||||
|
|||||||
@ -9,55 +9,58 @@ export type AcceptTeamInvitationOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const acceptTeamInvitation = async ({ userId, teamId }: AcceptTeamInvitationOptions) => {
|
export const acceptTeamInvitation = async ({ userId, teamId }: AcceptTeamInvitationOptions) => {
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(
|
||||||
const user = await tx.user.findFirstOrThrow({
|
async (tx) => {
|
||||||
where: {
|
const user = await tx.user.findFirstOrThrow({
|
||||||
id: userId,
|
where: {
|
||||||
},
|
id: userId,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const teamMemberInvite = await tx.teamMemberInvite.findFirstOrThrow({
|
const teamMemberInvite = await tx.teamMemberInvite.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
teamId,
|
teamId,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
team: {
|
team: {
|
||||||
include: {
|
include: {
|
||||||
subscription: true,
|
subscription: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const { team } = teamMemberInvite;
|
const { team } = teamMemberInvite;
|
||||||
|
|
||||||
await tx.teamMember.create({
|
await tx.teamMember.create({
|
||||||
data: {
|
data: {
|
||||||
teamId: teamMemberInvite.teamId,
|
|
||||||
userId: user.id,
|
|
||||||
role: teamMemberInvite.role,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.teamMemberInvite.delete({
|
|
||||||
where: {
|
|
||||||
id: teamMemberInvite.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (IS_BILLING_ENABLED() && team.subscription) {
|
|
||||||
const numberOfSeats = await tx.teamMember.count({
|
|
||||||
where: {
|
|
||||||
teamId: teamMemberInvite.teamId,
|
teamId: teamMemberInvite.teamId,
|
||||||
|
userId: user.id,
|
||||||
|
role: teamMemberInvite.role,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateSubscriptionItemQuantity({
|
await tx.teamMemberInvite.delete({
|
||||||
priceId: team.subscription.priceId,
|
where: {
|
||||||
subscriptionId: team.subscription.planId,
|
id: teamMemberInvite.id,
|
||||||
quantity: numberOfSeats,
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
if (IS_BILLING_ENABLED() && team.subscription) {
|
||||||
|
const numberOfSeats = await tx.teamMember.count({
|
||||||
|
where: {
|
||||||
|
teamId: teamMemberInvite.teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateSubscriptionItemQuantity({
|
||||||
|
priceId: team.subscription.priceId,
|
||||||
|
subscriptionId: team.subscription.planId,
|
||||||
|
quantity: numberOfSeats,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -28,56 +28,59 @@ export const createTeamEmailVerification = async ({
|
|||||||
data,
|
data,
|
||||||
}: CreateTeamEmailVerificationOptions) => {
|
}: CreateTeamEmailVerificationOptions) => {
|
||||||
try {
|
try {
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(
|
||||||
const team = await tx.team.findFirstOrThrow({
|
async (tx) => {
|
||||||
where: {
|
const team = await tx.team.findFirstOrThrow({
|
||||||
id: teamId,
|
where: {
|
||||||
members: {
|
id: teamId,
|
||||||
some: {
|
members: {
|
||||||
userId,
|
some: {
|
||||||
role: {
|
userId,
|
||||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
role: {
|
||||||
|
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
include: {
|
||||||
include: {
|
teamEmail: true,
|
||||||
teamEmail: true,
|
emailVerification: true,
|
||||||
emailVerification: true,
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (team.teamEmail || team.emailVerification) {
|
if (team.teamEmail || team.emailVerification) {
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
AppErrorCode.INVALID_REQUEST,
|
AppErrorCode.INVALID_REQUEST,
|
||||||
'Team already has an email or existing email verification.',
|
'Team already has an email or existing email verification.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingTeamEmail = await tx.teamEmail.findFirst({
|
const existingTeamEmail = await tx.teamEmail.findFirst({
|
||||||
where: {
|
where: {
|
||||||
email: data.email,
|
email: data.email,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingTeamEmail) {
|
if (existingTeamEmail) {
|
||||||
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Email already taken by another team.');
|
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Email already taken by another team.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token, expiresAt } = createTokenVerification({ hours: 1 });
|
const { token, expiresAt } = createTokenVerification({ hours: 1 });
|
||||||
|
|
||||||
await tx.teamEmailVerification.create({
|
await tx.teamEmailVerification.create({
|
||||||
data: {
|
data: {
|
||||||
token,
|
token,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
email: data.email,
|
email: data.email,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
teamId,
|
teamId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendTeamEmailVerificationEmail(data.email, token, team.name, team.url);
|
await sendTeamEmailVerificationEmail(data.email, token, team.name, team.url);
|
||||||
});
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
|
|||||||
@ -27,76 +27,81 @@ export const deleteTeamMembers = async ({
|
|||||||
teamId,
|
teamId,
|
||||||
teamMemberIds,
|
teamMemberIds,
|
||||||
}: DeleteTeamMembersOptions) => {
|
}: DeleteTeamMembersOptions) => {
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(
|
||||||
// Find the team and validate that the user is allowed to remove members.
|
async (tx) => {
|
||||||
const team = await tx.team.findFirstOrThrow({
|
// Find the team and validate that the user is allowed to remove members.
|
||||||
where: {
|
const team = await tx.team.findFirstOrThrow({
|
||||||
id: teamId,
|
where: {
|
||||||
members: {
|
id: teamId,
|
||||||
some: {
|
members: {
|
||||||
userId,
|
some: {
|
||||||
role: {
|
userId,
|
||||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
role: {
|
||||||
|
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
include: {
|
||||||
include: {
|
members: {
|
||||||
members: {
|
select: {
|
||||||
select: {
|
id: true,
|
||||||
id: true,
|
userId: true,
|
||||||
userId: true,
|
role: true,
|
||||||
role: true,
|
},
|
||||||
|
},
|
||||||
|
subscription: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentTeamMember = team.members.find((member) => member.userId === userId);
|
||||||
|
const teamMembersToRemove = team.members.filter((member) =>
|
||||||
|
teamMemberIds.includes(member.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentTeamMember) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, 'Team member record does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (teamMembersToRemove.find((member) => member.userId === team.ownerUserId)) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Cannot remove the team owner');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMemberToRemoveHigherRole = teamMembersToRemove.some(
|
||||||
|
(member) => !isTeamRoleWithinUserHierarchy(currentTeamMember.role, member.role),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMemberToRemoveHigherRole) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Cannot remove a member with a higher role');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the team members.
|
||||||
|
await tx.teamMember.deleteMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: teamMemberIds,
|
||||||
|
},
|
||||||
|
teamId,
|
||||||
|
userId: {
|
||||||
|
not: team.ownerUserId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
subscription: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentTeamMember = team.members.find((member) => member.userId === userId);
|
|
||||||
const teamMembersToRemove = team.members.filter((member) => teamMemberIds.includes(member.id));
|
|
||||||
|
|
||||||
if (!currentTeamMember) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, 'Team member record does not exist');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (teamMembersToRemove.find((member) => member.userId === team.ownerUserId)) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Cannot remove the team owner');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMemberToRemoveHigherRole = teamMembersToRemove.some(
|
|
||||||
(member) => !isTeamRoleWithinUserHierarchy(currentTeamMember.role, member.role),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isMemberToRemoveHigherRole) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Cannot remove a member with a higher role');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the team members.
|
|
||||||
await tx.teamMember.deleteMany({
|
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
in: teamMemberIds,
|
|
||||||
},
|
|
||||||
teamId,
|
|
||||||
userId: {
|
|
||||||
not: team.ownerUserId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (IS_BILLING_ENABLED() && team.subscription) {
|
|
||||||
const numberOfSeats = await tx.teamMember.count({
|
|
||||||
where: {
|
|
||||||
teamId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateSubscriptionItemQuantity({
|
if (IS_BILLING_ENABLED() && team.subscription) {
|
||||||
priceId: team.subscription.priceId,
|
const numberOfSeats = await tx.teamMember.count({
|
||||||
subscriptionId: team.subscription.planId,
|
where: {
|
||||||
quantity: numberOfSeats,
|
teamId,
|
||||||
});
|
},
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
await updateSubscriptionItemQuantity({
|
||||||
|
priceId: team.subscription.priceId,
|
||||||
|
subscriptionId: team.subscription.planId,
|
||||||
|
quantity: numberOfSeats,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,34 +9,37 @@ export type DeleteTeamOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => {
|
export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => {
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(
|
||||||
const team = await tx.team.findFirstOrThrow({
|
async (tx) => {
|
||||||
where: {
|
const team = await tx.team.findFirstOrThrow({
|
||||||
id: teamId,
|
where: {
|
||||||
ownerUserId: userId,
|
id: teamId,
|
||||||
},
|
ownerUserId: userId,
|
||||||
include: {
|
},
|
||||||
subscription: true,
|
include: {
|
||||||
},
|
subscription: true,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (team.subscription) {
|
if (team.subscription) {
|
||||||
await stripe.subscriptions
|
await stripe.subscriptions
|
||||||
.cancel(team.subscription.planId, {
|
.cancel(team.subscription.planId, {
|
||||||
prorate: false,
|
prorate: false,
|
||||||
invoice_now: true,
|
invoice_now: true,
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw AppError.parseError(err);
|
throw AppError.parseError(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.team.delete({
|
await tx.team.delete({
|
||||||
where: {
|
where: {
|
||||||
id: teamId,
|
id: teamId,
|
||||||
ownerUserId: userId,
|
ownerUserId: userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,45 +15,48 @@ export type LeaveTeamOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const leaveTeam = async ({ userId, teamId }: LeaveTeamOptions) => {
|
export const leaveTeam = async ({ userId, teamId }: LeaveTeamOptions) => {
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(
|
||||||
const team = await tx.team.findFirstOrThrow({
|
async (tx) => {
|
||||||
where: {
|
const team = await tx.team.findFirstOrThrow({
|
||||||
id: teamId,
|
where: {
|
||||||
ownerUserId: {
|
id: teamId,
|
||||||
not: userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
subscription: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.teamMember.delete({
|
|
||||||
where: {
|
|
||||||
userId_teamId: {
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
ownerUserId: {
|
ownerUserId: {
|
||||||
not: userId,
|
not: userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
include: {
|
||||||
});
|
subscription: true,
|
||||||
|
|
||||||
if (IS_BILLING_ENABLED() && team.subscription) {
|
|
||||||
const numberOfSeats = await tx.teamMember.count({
|
|
||||||
where: {
|
|
||||||
teamId,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateSubscriptionItemQuantity({
|
await tx.teamMember.delete({
|
||||||
priceId: team.subscription.priceId,
|
where: {
|
||||||
subscriptionId: team.subscription.planId,
|
userId_teamId: {
|
||||||
quantity: numberOfSeats,
|
userId,
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
ownerUserId: {
|
||||||
|
not: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
if (IS_BILLING_ENABLED() && team.subscription) {
|
||||||
|
const numberOfSeats = await tx.teamMember.count({
|
||||||
|
where: {
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateSubscriptionItemQuantity({
|
||||||
|
priceId: team.subscription.priceId,
|
||||||
|
subscriptionId: team.subscription.planId,
|
||||||
|
quantity: numberOfSeats,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -44,63 +44,66 @@ export const requestTeamOwnershipTransfer = async ({
|
|||||||
// Todo: Clear payment methods disabled for now.
|
// Todo: Clear payment methods disabled for now.
|
||||||
const clearPaymentMethods = false;
|
const clearPaymentMethods = false;
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(
|
||||||
const team = await tx.team.findFirstOrThrow({
|
async (tx) => {
|
||||||
where: {
|
const team = await tx.team.findFirstOrThrow({
|
||||||
id: teamId,
|
where: {
|
||||||
ownerUserId: userId,
|
id: teamId,
|
||||||
members: {
|
ownerUserId: userId,
|
||||||
some: {
|
members: {
|
||||||
userId: newOwnerUserId,
|
some: {
|
||||||
|
userId: newOwnerUserId,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const newOwnerUser = await tx.user.findFirstOrThrow({
|
const newOwnerUser = await tx.user.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: newOwnerUserId,
|
id: newOwnerUserId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { token, expiresAt } = createTokenVerification({ minute: 10 });
|
const { token, expiresAt } = createTokenVerification({ minute: 10 });
|
||||||
|
|
||||||
const teamVerificationPayload = {
|
const teamVerificationPayload = {
|
||||||
teamId,
|
|
||||||
token,
|
|
||||||
expiresAt,
|
|
||||||
userId: newOwnerUserId,
|
|
||||||
name: newOwnerUser.name ?? '',
|
|
||||||
email: newOwnerUser.email,
|
|
||||||
clearPaymentMethods,
|
|
||||||
};
|
|
||||||
|
|
||||||
await tx.teamTransferVerification.upsert({
|
|
||||||
where: {
|
|
||||||
teamId,
|
teamId,
|
||||||
},
|
token,
|
||||||
create: teamVerificationPayload,
|
expiresAt,
|
||||||
update: teamVerificationPayload,
|
userId: newOwnerUserId,
|
||||||
});
|
name: newOwnerUser.name ?? '',
|
||||||
|
email: newOwnerUser.email,
|
||||||
|
clearPaymentMethods,
|
||||||
|
};
|
||||||
|
|
||||||
const template = createElement(TeamTransferRequestTemplate, {
|
await tx.teamTransferVerification.upsert({
|
||||||
assetBaseUrl: WEBAPP_BASE_URL,
|
where: {
|
||||||
baseUrl: WEBAPP_BASE_URL,
|
teamId,
|
||||||
senderName: userName,
|
},
|
||||||
teamName: team.name,
|
create: teamVerificationPayload,
|
||||||
teamUrl: team.url,
|
update: teamVerificationPayload,
|
||||||
token,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
await mailer.sendMail({
|
const template = createElement(TeamTransferRequestTemplate, {
|
||||||
to: newOwnerUser.email,
|
assetBaseUrl: WEBAPP_BASE_URL,
|
||||||
from: {
|
baseUrl: WEBAPP_BASE_URL,
|
||||||
name: FROM_NAME,
|
senderName: userName,
|
||||||
address: FROM_ADDRESS,
|
teamName: team.name,
|
||||||
},
|
teamUrl: team.url,
|
||||||
subject: `You have been requested to take ownership of team ${team.name} on Documenso`,
|
token,
|
||||||
html: render(template),
|
});
|
||||||
text: render(template, { plainText: true }),
|
|
||||||
});
|
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 }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,49 +17,52 @@ export const resendTeamEmailVerification = async ({
|
|||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
}: ResendTeamMemberInvitationOptions) => {
|
}: ResendTeamMemberInvitationOptions) => {
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(
|
||||||
const team = await tx.team.findUniqueOrThrow({
|
async (tx) => {
|
||||||
where: {
|
const team = await tx.team.findUniqueOrThrow({
|
||||||
id: teamId,
|
where: {
|
||||||
members: {
|
id: teamId,
|
||||||
some: {
|
members: {
|
||||||
userId,
|
some: {
|
||||||
role: {
|
userId,
|
||||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
role: {
|
||||||
|
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
include: {
|
||||||
include: {
|
emailVerification: true,
|
||||||
emailVerification: true,
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (!team) {
|
if (!team) {
|
||||||
throw new AppError('TeamNotFound', 'User is not a member of the team.');
|
throw new AppError('TeamNotFound', 'User is not a member of the team.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { emailVerification } = team;
|
const { emailVerification } = team;
|
||||||
|
|
||||||
if (!emailVerification) {
|
if (!emailVerification) {
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
'VerificationNotFound',
|
'VerificationNotFound',
|
||||||
'No team email verification exists for this team.',
|
'No team email verification exists for this team.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token, expiresAt } = createTokenVerification({ hours: 1 });
|
const { token, expiresAt } = createTokenVerification({ hours: 1 });
|
||||||
|
|
||||||
await tx.teamEmailVerification.update({
|
await tx.teamEmailVerification.update({
|
||||||
where: {
|
where: {
|
||||||
teamId,
|
teamId,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
token,
|
token,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendTeamEmailVerificationEmail(emailVerification.email, token, team.name, team.url);
|
await sendTeamEmailVerificationEmail(emailVerification.email, token, team.name, team.url);
|
||||||
});
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -35,42 +35,45 @@ export const resendTeamMemberInvitation = async ({
|
|||||||
teamId,
|
teamId,
|
||||||
invitationId,
|
invitationId,
|
||||||
}: ResendTeamMemberInvitationOptions) => {
|
}: ResendTeamMemberInvitationOptions) => {
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(
|
||||||
const team = await tx.team.findUniqueOrThrow({
|
async (tx) => {
|
||||||
where: {
|
const team = await tx.team.findUniqueOrThrow({
|
||||||
id: teamId,
|
where: {
|
||||||
members: {
|
id: teamId,
|
||||||
some: {
|
members: {
|
||||||
userId,
|
some: {
|
||||||
role: {
|
userId,
|
||||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
role: {
|
||||||
|
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (!team) {
|
if (!team) {
|
||||||
throw new AppError('TeamNotFound', 'User is not a valid member of the team.');
|
throw new AppError('TeamNotFound', 'User is not a valid member of the team.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const teamMemberInvite = await tx.teamMemberInvite.findUniqueOrThrow({
|
const teamMemberInvite = await tx.teamMemberInvite.findUniqueOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: invitationId,
|
id: invitationId,
|
||||||
teamId,
|
teamId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!teamMemberInvite) {
|
if (!teamMemberInvite) {
|
||||||
throw new AppError('InviteNotFound', 'No invite exists for this user.');
|
throw new AppError('InviteNotFound', 'No invite exists for this user.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendTeamMemberInviteEmail({
|
await sendTeamMemberInviteEmail({
|
||||||
email: teamMemberInvite.email,
|
email: teamMemberInvite.email,
|
||||||
token: teamMemberInvite.token,
|
token: teamMemberInvite.token,
|
||||||
teamName: team.name,
|
teamName: team.name,
|
||||||
teamUrl: team.url,
|
teamUrl: team.url,
|
||||||
senderName: userName,
|
senderName: userName,
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,78 +11,81 @@ export type TransferTeamOwnershipOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const transferTeamOwnership = async ({ token }: TransferTeamOwnershipOptions) => {
|
export const transferTeamOwnership = async ({ token }: TransferTeamOwnershipOptions) => {
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(
|
||||||
const teamTransferVerification = await tx.teamTransferVerification.findFirstOrThrow({
|
async (tx) => {
|
||||||
where: {
|
const teamTransferVerification = await tx.teamTransferVerification.findFirstOrThrow({
|
||||||
token,
|
where: {
|
||||||
},
|
token,
|
||||||
include: {
|
},
|
||||||
team: {
|
include: {
|
||||||
include: {
|
team: {
|
||||||
subscription: true,
|
include: {
|
||||||
|
subscription: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { team, userId: newOwnerUserId } = teamTransferVerification;
|
|
||||||
|
|
||||||
await tx.teamTransferVerification.delete({
|
|
||||||
where: {
|
|
||||||
teamId: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const newOwnerUser = await tx.user.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: newOwnerUserId,
|
|
||||||
teamMembers: {
|
|
||||||
some: {
|
|
||||||
teamId: team.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Subscription: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let teamSubscription: Stripe.Subscription | null = null;
|
|
||||||
|
|
||||||
if (IS_BILLING_ENABLED()) {
|
|
||||||
teamSubscription = await transferTeamSubscription({
|
|
||||||
user: newOwnerUser,
|
|
||||||
team,
|
|
||||||
clearPaymentMethods: teamTransferVerification.clearPaymentMethods,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (teamSubscription) {
|
const { team, userId: newOwnerUserId } = teamTransferVerification;
|
||||||
await tx.subscription.upsert(
|
|
||||||
mapStripeSubscriptionToPrismaUpsertAction(teamSubscription, undefined, team.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx.team.update({
|
await tx.teamTransferVerification.delete({
|
||||||
where: {
|
where: {
|
||||||
id: team.id,
|
teamId: team.id,
|
||||||
},
|
},
|
||||||
data: {
|
});
|
||||||
ownerUserId: newOwnerUserId,
|
|
||||||
members: {
|
const newOwnerUser = await tx.user.findFirstOrThrow({
|
||||||
update: {
|
where: {
|
||||||
where: {
|
id: newOwnerUserId,
|
||||||
userId_teamId: {
|
teamMembers: {
|
||||||
teamId: team.id,
|
some: {
|
||||||
userId: newOwnerUserId,
|
teamId: team.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
Subscription: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let teamSubscription: Stripe.Subscription | null = null;
|
||||||
|
|
||||||
|
if (IS_BILLING_ENABLED()) {
|
||||||
|
teamSubscription = await transferTeamSubscription({
|
||||||
|
user: newOwnerUser,
|
||||||
|
team,
|
||||||
|
clearPaymentMethods: teamTransferVerification.clearPaymentMethods,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (teamSubscription) {
|
||||||
|
await tx.subscription.upsert(
|
||||||
|
mapStripeSubscriptionToPrismaUpsertAction(teamSubscription, undefined, team.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.team.update({
|
||||||
|
where: {
|
||||||
|
id: team.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
ownerUserId: newOwnerUserId,
|
||||||
|
members: {
|
||||||
|
update: {
|
||||||
|
where: {
|
||||||
|
userId_teamId: {
|
||||||
|
teamId: team.id,
|
||||||
|
userId: newOwnerUserId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
role: TeamMemberRole.ADMIN,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
|
||||||
role: TeamMemberRole.ADMIN,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
},
|
||||||
});
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -53,47 +53,50 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
|
|||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
acceptedTeamInvites.map(async (invite) =>
|
acceptedTeamInvites.map(async (invite) =>
|
||||||
prisma
|
prisma
|
||||||
.$transaction(async (tx) => {
|
.$transaction(
|
||||||
await tx.teamMember.create({
|
async (tx) => {
|
||||||
data: {
|
await tx.teamMember.create({
|
||||||
teamId: invite.teamId,
|
data: {
|
||||||
userId: user.id,
|
teamId: invite.teamId,
|
||||||
role: invite.role,
|
userId: user.id,
|
||||||
},
|
role: invite.role,
|
||||||
});
|
|
||||||
|
|
||||||
await tx.teamMemberInvite.delete({
|
|
||||||
where: {
|
|
||||||
id: invite.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!IS_BILLING_ENABLED()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const team = await tx.team.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: invite.teamId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
members: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
subscription: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (team.subscription) {
|
|
||||||
await updateSubscriptionItemQuantity({
|
|
||||||
priceId: team.subscription.priceId,
|
|
||||||
subscriptionId: team.subscription.planId,
|
|
||||||
quantity: team.members.length,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
})
|
await tx.teamMemberInvite.delete({
|
||||||
|
where: {
|
||||||
|
id: invite.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!IS_BILLING_ENABLED()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const team = await tx.team.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: invite.teamId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
subscription: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (team.subscription) {
|
||||||
|
await updateSubscriptionItemQuantity({
|
||||||
|
priceId: team.subscription.priceId,
|
||||||
|
subscriptionId: team.subscription.planId,
|
||||||
|
quantity: team.members.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
)
|
||||||
.catch(async () => {
|
.catch(async () => {
|
||||||
await prisma.teamMemberInvite.update({
|
await prisma.teamMemberInvite.update({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
Reference in New Issue
Block a user