From a48bda0d273acf22a81d7000a79d7e11d363797e Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+dephraiim@users.noreply.github.com> Date: Thu, 22 Feb 2024 05:36:34 +0000 Subject: [PATCH 1/3] feat: add page numbers to documents (#946) --- packages/ui/primitives/pdf-viewer.tsx | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/ui/primitives/pdf-viewer.tsx b/packages/ui/primitives/pdf-viewer.tsx index b4e5c10ba..1069290e6 100644 --- a/packages/ui/primitives/pdf-viewer.tsx +++ b/packages/ui/primitives/pdf-viewer.tsx @@ -233,18 +233,20 @@ export const PDFViewer = ({ {Array(numPages) .fill(null) .map((_, i) => ( -
- ''} - onClick={(e) => onDocumentPageClick(e, i + 1)} - /> +
+
+ ''} + onClick={(e) => onDocumentPageClick(e, i + 1)} + /> +
+

+ Page {i + 1} of {numPages} +

))} From dd29845934985a046ec1ab8255d604ef2e2deef8 Mon Sep 17 00:00:00 2001 From: Sumit Bisht <75713174+sumitbishti@users.noreply.github.com> Date: Thu, 22 Feb 2024 11:19:54 +0530 Subject: [PATCH 2/3] fix: put max height in teams section in profile dropdown (#947) fixes: #942 --- .../(dashboard)/layout/menu-switcher.tsx | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx b/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx index 195716d64..765343d27 100644 --- a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx +++ b/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx @@ -166,22 +166,24 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
- {teams.map((team) => ( - - - - ) - } - /> - - - ))} +
+ {teams.map((team) => ( + + + + ) + } + /> + + + ))} +
) : ( From 306e5ff31f01cd2b168550e159b84e505163ac34 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Thu, 22 Feb 2024 11:05:49 +0000 Subject: [PATCH 3/3] fix: add timeouts to longer transactions --- .../server-only/document/resend-document.tsx | 72 +++++----- .../document/send-completed-email.ts | 75 +++++----- .../server-only/document/send-document.tsx | 87 +++++------ .../lib/server-only/document/update-title.ts | 44 +++--- .../team/accept-team-invitation.ts | 85 +++++------ .../team/create-team-email-verification.ts | 85 +++++------ .../server-only/team/delete-team-members.ts | 135 +++++++++--------- packages/lib/server-only/team/delete-team.ts | 59 ++++---- packages/lib/server-only/team/leave-team.ts | 69 ++++----- .../team/request-team-ownership-transfer.ts | 105 +++++++------- .../team/resend-team-email-verification.ts | 75 +++++----- .../team/resend-team-member-invitation.ts | 65 +++++---- .../team/transfer-team-ownership.ts | 133 ++++++++--------- packages/lib/server-only/user/create-user.ts | 81 ++++++----- 14 files changed, 607 insertions(+), 563 deletions(-) diff --git a/packages/lib/server-only/document/resend-document.tsx b/packages/lib/server-only/document/resend-document.tsx index 0dbda803e..ebf140007 100644 --- a/packages/lib/server-only/document/resend-document.tsx +++ b/packages/lib/server-only/document/resend-document.tsx @@ -16,9 +16,8 @@ import { prisma } from '@documenso/prisma'; import { DocumentStatus, RecipientRole, SigningStatus } 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 { getDocumentWhereInput } from './get-document-by-id'; export type ResendDocumentOptions = { documentId: number; @@ -111,40 +110,43 @@ export const resendDocument = async ({ const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role]; - await prisma.$transaction(async (tx) => { - await mailer.sendMail({ - to: { - 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, + await prisma.$transaction( + async (tx) => { + await mailer.sendMail({ + to: { + 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, + }, + }), + }); + }, + { timeout: 30_000 }, + ); }), ); }; diff --git a/packages/lib/server-only/document/send-completed-email.ts b/packages/lib/server-only/document/send-completed-email.ts index ad8207303..812e54ba3 100644 --- a/packages/lib/server-only/document/send-completed-email.ts +++ b/packages/lib/server-only/document/send-completed-email.ts @@ -49,44 +49,47 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo downloadLink: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${token}/complete`, }); - await prisma.$transaction(async (tx) => { - await mailer.sendMail({ - to: { - 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), + await prisma.$transaction( + async (tx) => { + await mailer.sendMail({ + to: { + 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), + }, + ], + }); - await tx.documentAuditLog.create({ - data: createDocumentAuditLogData({ - type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT, - documentId: document.id, - user: null, - requestMetadata, - data: { - emailType: 'DOCUMENT_COMPLETED', - recipientEmail: recipient.email, - recipientName: recipient.name, - recipientId: recipient.id, - recipientRole: recipient.role, - isResending: false, - }, - }), - }); - }); + await tx.documentAuditLog.create({ + data: createDocumentAuditLogData({ + type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT, + documentId: document.id, + user: null, + requestMetadata, + data: { + emailType: 'DOCUMENT_COMPLETED', + recipientEmail: recipient.email, + recipientName: recipient.name, + recipientId: recipient.id, + recipientRole: recipient.role, + isResending: false, + }, + }), + }); + }, + { timeout: 30_000 }, + ); }), ); }; diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index be26ffcaf..4d85a8f32 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -108,49 +108,52 @@ export const sendDocument = async ({ const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role]; - await prisma.$transaction(async (tx) => { - await mailer.sendMail({ - to: { - 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, + await prisma.$transaction( + async (tx) => { + await mailer.sendMail({ + to: { + 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, + }, + }), + }); + }, + { timeout: 30_000 }, + ); }), ); diff --git a/packages/lib/server-only/document/update-title.ts b/packages/lib/server-only/document/update-title.ts index 3e934e7be..f7f7a6b88 100644 --- a/packages/lib/server-only/document/update-title.ts +++ b/packages/lib/server-only/document/update-title.ts @@ -24,34 +24,38 @@ export const updateTitle = async ({ }, }); - return await prisma.$transaction(async (tx) => { - const document = await tx.document.findFirstOrThrow({ - where: { - id: documentId, - OR: [ - { - userId, - }, - { - team: { - members: { - some: { - userId, - }, + const document = await prisma.document.findFirstOrThrow({ + where: { + id: documentId, + OR: [ + { + userId, + }, + { + team: { + members: { + some: { + userId, }, }, }, - ], - }, - }); + }, + ], + }, + }); - if (document.title === title) { - return document; - } + if (document.title === title) { + 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({ where: { id: documentId, + title: document.title, }, data: { title, diff --git a/packages/lib/server-only/team/accept-team-invitation.ts b/packages/lib/server-only/team/accept-team-invitation.ts index 4bfd31dfa..31fef5967 100644 --- a/packages/lib/server-only/team/accept-team-invitation.ts +++ b/packages/lib/server-only/team/accept-team-invitation.ts @@ -9,55 +9,58 @@ export type AcceptTeamInvitationOptions = { }; export const acceptTeamInvitation = async ({ userId, teamId }: AcceptTeamInvitationOptions) => { - await prisma.$transaction(async (tx) => { - const user = await tx.user.findFirstOrThrow({ - where: { - id: userId, - }, - }); + await prisma.$transaction( + async (tx) => { + const user = await tx.user.findFirstOrThrow({ + where: { + id: userId, + }, + }); - const teamMemberInvite = await tx.teamMemberInvite.findFirstOrThrow({ - where: { - teamId, - email: user.email, - }, - include: { - team: { - include: { - subscription: true, + const teamMemberInvite = await tx.teamMemberInvite.findFirstOrThrow({ + where: { + teamId, + email: user.email, + }, + include: { + team: { + include: { + subscription: true, + }, }, }, - }, - }); + }); - const { team } = teamMemberInvite; + const { team } = teamMemberInvite; - await tx.teamMember.create({ - 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: { + await tx.teamMember.create({ + data: { teamId: teamMemberInvite.teamId, + userId: user.id, + role: teamMemberInvite.role, }, }); - await updateSubscriptionItemQuantity({ - priceId: team.subscription.priceId, - subscriptionId: team.subscription.planId, - quantity: numberOfSeats, + await tx.teamMemberInvite.delete({ + where: { + id: teamMemberInvite.id, + }, }); - } - }); + + 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 }, + ); }; diff --git a/packages/lib/server-only/team/create-team-email-verification.ts b/packages/lib/server-only/team/create-team-email-verification.ts index 28e1538d0..86cded7a9 100644 --- a/packages/lib/server-only/team/create-team-email-verification.ts +++ b/packages/lib/server-only/team/create-team-email-verification.ts @@ -28,56 +28,59 @@ export const createTeamEmailVerification = async ({ 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'], + 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, - }, - }); + 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.', - ); - } + 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, - }, - }); + const existingTeamEmail = await tx.teamEmail.findFirst({ + where: { + email: data.email, + }, + }); - if (existingTeamEmail) { - throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Email already taken by another team.'); - } + if (existingTeamEmail) { + 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({ - data: { - token, - expiresAt, - email: data.email, - name: data.name, - teamId, - }, - }); + await tx.teamEmailVerification.create({ + data: { + token, + expiresAt, + email: data.email, + name: data.name, + teamId, + }, + }); - await sendTeamEmailVerificationEmail(data.email, token, team.name, team.url); - }); + await sendTeamEmailVerificationEmail(data.email, token, team.name, team.url); + }, + { timeout: 30_000 }, + ); } catch (err) { console.error(err); diff --git a/packages/lib/server-only/team/delete-team-members.ts b/packages/lib/server-only/team/delete-team-members.ts index 162d7de53..14b75a473 100644 --- a/packages/lib/server-only/team/delete-team-members.ts +++ b/packages/lib/server-only/team/delete-team-members.ts @@ -27,76 +27,81 @@ export const deleteTeamMembers = async ({ teamId, teamMemberIds, }: DeleteTeamMembersOptions) => { - await prisma.$transaction(async (tx) => { - // Find the team and validate that the user is allowed to remove members. - const team = await tx.team.findFirstOrThrow({ - where: { - id: teamId, - members: { - some: { - userId, - role: { - in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'], + await prisma.$transaction( + async (tx) => { + // Find the team and validate that the user is allowed to remove members. + const team = await tx.team.findFirstOrThrow({ + where: { + id: teamId, + members: { + some: { + userId, + role: { + in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'], + }, }, }, }, - }, - include: { - members: { - select: { - id: true, - userId: true, - role: true, + include: { + members: { + select: { + id: true, + userId: 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({ - priceId: team.subscription.priceId, - subscriptionId: team.subscription.planId, - quantity: numberOfSeats, - }); - } - }); + 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 }, + ); }; diff --git a/packages/lib/server-only/team/delete-team.ts b/packages/lib/server-only/team/delete-team.ts index dffc044d8..667d2f448 100644 --- a/packages/lib/server-only/team/delete-team.ts +++ b/packages/lib/server-only/team/delete-team.ts @@ -9,34 +9,37 @@ export type DeleteTeamOptions = { }; export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => { - await prisma.$transaction(async (tx) => { - const team = await tx.team.findFirstOrThrow({ - where: { - id: teamId, - ownerUserId: userId, - }, - include: { - subscription: true, - }, - }); + await prisma.$transaction( + async (tx) => { + const team = await tx.team.findFirstOrThrow({ + where: { + id: teamId, + ownerUserId: userId, + }, + include: { + subscription: true, + }, + }); - if (team.subscription) { - await stripe.subscriptions - .cancel(team.subscription.planId, { - prorate: false, - invoice_now: true, - }) - .catch((err) => { - console.error(err); - throw AppError.parseError(err); - }); - } + if (team.subscription) { + await stripe.subscriptions + .cancel(team.subscription.planId, { + prorate: false, + invoice_now: true, + }) + .catch((err) => { + console.error(err); + throw AppError.parseError(err); + }); + } - await tx.team.delete({ - where: { - id: teamId, - ownerUserId: userId, - }, - }); - }); + await tx.team.delete({ + where: { + id: teamId, + ownerUserId: userId, + }, + }); + }, + { timeout: 30_000 }, + ); }; diff --git a/packages/lib/server-only/team/leave-team.ts b/packages/lib/server-only/team/leave-team.ts index 509c38950..410d0707c 100644 --- a/packages/lib/server-only/team/leave-team.ts +++ b/packages/lib/server-only/team/leave-team.ts @@ -15,45 +15,48 @@ export type LeaveTeamOptions = { }; export const leaveTeam = async ({ userId, teamId }: LeaveTeamOptions) => { - await prisma.$transaction(async (tx) => { - const team = await tx.team.findFirstOrThrow({ - where: { - id: teamId, - ownerUserId: { - not: userId, - }, - }, - include: { - subscription: true, - }, - }); - - await tx.teamMember.delete({ - where: { - userId_teamId: { - userId, - teamId, - }, - team: { + await prisma.$transaction( + async (tx) => { + const team = await tx.team.findFirstOrThrow({ + where: { + id: teamId, ownerUserId: { not: userId, }, }, - }, - }); - - if (IS_BILLING_ENABLED() && team.subscription) { - const numberOfSeats = await tx.teamMember.count({ - where: { - teamId, + include: { + subscription: true, }, }); - await updateSubscriptionItemQuantity({ - priceId: team.subscription.priceId, - subscriptionId: team.subscription.planId, - quantity: numberOfSeats, + await tx.teamMember.delete({ + where: { + userId_teamId: { + 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 }, + ); }; diff --git a/packages/lib/server-only/team/request-team-ownership-transfer.ts b/packages/lib/server-only/team/request-team-ownership-transfer.ts index 7da976ee1..92fd5b61e 100644 --- a/packages/lib/server-only/team/request-team-ownership-transfer.ts +++ b/packages/lib/server-only/team/request-team-ownership-transfer.ts @@ -44,63 +44,66 @@ export const requestTeamOwnershipTransfer = async ({ // Todo: Clear payment methods disabled for now. const clearPaymentMethods = false; - await prisma.$transaction(async (tx) => { - const team = await tx.team.findFirstOrThrow({ - where: { - id: teamId, - ownerUserId: userId, - members: { - some: { - userId: newOwnerUserId, + await prisma.$transaction( + async (tx) => { + const team = await tx.team.findFirstOrThrow({ + where: { + id: teamId, + ownerUserId: userId, + members: { + some: { + userId: newOwnerUserId, + }, }, }, - }, - }); + }); - const newOwnerUser = await tx.user.findFirstOrThrow({ - where: { - id: newOwnerUserId, - }, - }); + const newOwnerUser = await tx.user.findFirstOrThrow({ + where: { + id: newOwnerUserId, + }, + }); - const { token, expiresAt } = createTokenVerification({ minute: 10 }); + const { token, expiresAt } = createTokenVerification({ minute: 10 }); - const teamVerificationPayload = { - teamId, - token, - expiresAt, - userId: newOwnerUserId, - name: newOwnerUser.name ?? '', - email: newOwnerUser.email, - clearPaymentMethods, - }; - - await tx.teamTransferVerification.upsert({ - where: { + const teamVerificationPayload = { teamId, - }, - create: teamVerificationPayload, - update: teamVerificationPayload, - }); + token, + expiresAt, + userId: newOwnerUserId, + name: newOwnerUser.name ?? '', + email: newOwnerUser.email, + clearPaymentMethods, + }; - const template = createElement(TeamTransferRequestTemplate, { - assetBaseUrl: WEBAPP_BASE_URL, - baseUrl: WEBAPP_BASE_URL, - senderName: userName, - teamName: team.name, - teamUrl: team.url, - token, - }); + await tx.teamTransferVerification.upsert({ + where: { + teamId, + }, + create: teamVerificationPayload, + update: teamVerificationPayload, + }); - 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 }), - }); - }); + const template = createElement(TeamTransferRequestTemplate, { + assetBaseUrl: WEBAPP_BASE_URL, + baseUrl: WEBAPP_BASE_URL, + senderName: userName, + teamName: team.name, + teamUrl: team.url, + token, + }); + + 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 }, + ); }; diff --git a/packages/lib/server-only/team/resend-team-email-verification.ts b/packages/lib/server-only/team/resend-team-email-verification.ts index 55afe61ce..b1d6ffe99 100644 --- a/packages/lib/server-only/team/resend-team-email-verification.ts +++ b/packages/lib/server-only/team/resend-team-email-verification.ts @@ -17,49 +17,52 @@ export const resendTeamEmailVerification = async ({ userId, teamId, }: ResendTeamMemberInvitationOptions) => { - await prisma.$transaction(async (tx) => { - const team = await tx.team.findUniqueOrThrow({ - where: { - id: teamId, - members: { - some: { - userId, - role: { - in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'], + await prisma.$transaction( + async (tx) => { + const team = await tx.team.findUniqueOrThrow({ + where: { + id: teamId, + members: { + some: { + userId, + role: { + in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'], + }, }, }, }, - }, - include: { - emailVerification: true, - }, - }); + include: { + emailVerification: true, + }, + }); - if (!team) { - throw new AppError('TeamNotFound', 'User is not a member of the team.'); - } + if (!team) { + throw new AppError('TeamNotFound', 'User is not a member of the team.'); + } - const { emailVerification } = team; + const { emailVerification } = team; - if (!emailVerification) { - throw new AppError( - 'VerificationNotFound', - 'No team email verification exists for this team.', - ); - } + if (!emailVerification) { + throw new AppError( + 'VerificationNotFound', + 'No team email verification exists for this team.', + ); + } - const { token, expiresAt } = createTokenVerification({ hours: 1 }); + const { token, expiresAt } = createTokenVerification({ hours: 1 }); - await tx.teamEmailVerification.update({ - where: { - teamId, - }, - data: { - token, - expiresAt, - }, - }); + await tx.teamEmailVerification.update({ + where: { + teamId, + }, + data: { + token, + expiresAt, + }, + }); - await sendTeamEmailVerificationEmail(emailVerification.email, token, team.name, team.url); - }); + await sendTeamEmailVerificationEmail(emailVerification.email, token, team.name, team.url); + }, + { timeout: 30_000 }, + ); }; diff --git a/packages/lib/server-only/team/resend-team-member-invitation.ts b/packages/lib/server-only/team/resend-team-member-invitation.ts index fb860ccc0..897154a2a 100644 --- a/packages/lib/server-only/team/resend-team-member-invitation.ts +++ b/packages/lib/server-only/team/resend-team-member-invitation.ts @@ -35,42 +35,45 @@ export const resendTeamMemberInvitation = async ({ teamId, invitationId, }: ResendTeamMemberInvitationOptions) => { - await prisma.$transaction(async (tx) => { - const team = await tx.team.findUniqueOrThrow({ - where: { - id: teamId, - members: { - some: { - userId, - role: { - in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'], + await prisma.$transaction( + async (tx) => { + const team = await tx.team.findUniqueOrThrow({ + where: { + id: teamId, + members: { + some: { + userId, + role: { + in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'], + }, }, }, }, - }, - }); + }); - if (!team) { - throw new AppError('TeamNotFound', 'User is not a valid member of the team.'); - } + if (!team) { + throw new AppError('TeamNotFound', 'User is not a valid member of the team.'); + } - const teamMemberInvite = await tx.teamMemberInvite.findUniqueOrThrow({ - where: { - id: invitationId, - teamId, - }, - }); + const teamMemberInvite = await tx.teamMemberInvite.findUniqueOrThrow({ + where: { + id: invitationId, + teamId, + }, + }); - if (!teamMemberInvite) { - throw new AppError('InviteNotFound', 'No invite exists for this user.'); - } + if (!teamMemberInvite) { + throw new AppError('InviteNotFound', 'No invite exists for this user.'); + } - await sendTeamMemberInviteEmail({ - email: teamMemberInvite.email, - token: teamMemberInvite.token, - teamName: team.name, - teamUrl: team.url, - senderName: userName, - }); - }); + await sendTeamMemberInviteEmail({ + email: teamMemberInvite.email, + token: teamMemberInvite.token, + teamName: team.name, + teamUrl: team.url, + senderName: userName, + }); + }, + { timeout: 30_000 }, + ); }; diff --git a/packages/lib/server-only/team/transfer-team-ownership.ts b/packages/lib/server-only/team/transfer-team-ownership.ts index 7b886d323..b87c04b91 100644 --- a/packages/lib/server-only/team/transfer-team-ownership.ts +++ b/packages/lib/server-only/team/transfer-team-ownership.ts @@ -11,78 +11,81 @@ export type TransferTeamOwnershipOptions = { }; export const transferTeamOwnership = async ({ token }: TransferTeamOwnershipOptions) => { - await prisma.$transaction(async (tx) => { - const teamTransferVerification = await tx.teamTransferVerification.findFirstOrThrow({ - where: { - token, - }, - include: { - team: { - include: { - subscription: true, + await prisma.$transaction( + async (tx) => { + const teamTransferVerification = await tx.teamTransferVerification.findFirstOrThrow({ + where: { + token, + }, + include: { + team: { + 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) { - await tx.subscription.upsert( - mapStripeSubscriptionToPrismaUpsertAction(teamSubscription, undefined, team.id), - ); - } + const { team, userId: newOwnerUserId } = teamTransferVerification; - await tx.team.update({ - where: { - id: team.id, - }, - data: { - ownerUserId: newOwnerUserId, - members: { - update: { - where: { - userId_teamId: { - teamId: team.id, - userId: newOwnerUserId, + 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) { + 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 }, + ); }; diff --git a/packages/lib/server-only/user/create-user.ts b/packages/lib/server-only/user/create-user.ts index 9a40696db..1852dc12e 100644 --- a/packages/lib/server-only/user/create-user.ts +++ b/packages/lib/server-only/user/create-user.ts @@ -53,47 +53,50 @@ export const createUser = async ({ name, email, password, signature }: CreateUse await Promise.allSettled( acceptedTeamInvites.map(async (invite) => prisma - .$transaction(async (tx) => { - await tx.teamMember.create({ - data: { - teamId: invite.teamId, - 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, - }, + .$transaction( + async (tx) => { + await tx.teamMember.create({ + data: { + teamId: invite.teamId, + userId: user.id, + role: invite.role, }, - 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 () => { await prisma.teamMemberInvite.update({ where: {