import { randomBytes } from "crypto"; import { sendTeamInviteEmail } from "@calcom/emails"; import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { getTranslation } from "@calcom/lib/server/i18n"; import { prisma } from "@calcom/prisma"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import { isEmail } from "../util"; import type { TInviteMemberInputSchema } from "./inviteMember.schema"; import { checkPermissions, getTeamOrThrow, getEmailsToInvite, getUserToInviteOrThrowIfExists, checkInputEmailIsValid, getOrgConnectionInfo, createNewUserConnectToOrgIfExists, throwIfInviteIsToOrgAndUserExists, createProvisionalMembership, getIsOrgVerified, sendVerificationEmail, createAndAutoJoinIfInOrg, } from "./utils"; type InviteMemberOptions = { ctx: { user: NonNullable; }; input: TInviteMemberInputSchema; }; export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) => { const team = await getTeamOrThrow(input.teamId, input.isOrg); const { autoAcceptEmailDomain, orgVerified } = getIsOrgVerified(input.isOrg, team); await checkPermissions({ userId: ctx.user.id, teamId: ctx.user.organization.id && ctx.user.organization.isOrgAdmin ? ctx.user.organization.id : input.teamId, isOrg: input.isOrg, }); const translation = await getTranslation(input.language ?? "en", "common"); const emailsToInvite = await getEmailsToInvite(input.usernameOrEmail); for (const usernameOrEmail of emailsToInvite) { const connectionInfo = getOrgConnectionInfo({ orgVerified, orgAutoAcceptDomain: autoAcceptEmailDomain, usersEmail: usernameOrEmail, team, isOrg: input.isOrg, }); const invitee = await getUserToInviteOrThrowIfExists({ usernameOrEmail, teamId: input.teamId, isOrg: input.isOrg, }); if (!invitee) { checkInputEmailIsValid(usernameOrEmail); // valid email given, create User and add to team await createNewUserConnectToOrgIfExists({ usernameOrEmail, input, connectionInfo, parentId: team.parentId, }); await sendVerificationEmail({ usernameOrEmail, team, translation, ctx, input, connectionInfo }); } else { throwIfInviteIsToOrgAndUserExists(invitee, team, input.isOrg); const shouldAutoJoinOrgTeam = await createAndAutoJoinIfInOrg({ invitee, role: input.role, team, }); if (shouldAutoJoinOrgTeam.autoJoined) { // Continue here because if this is true we dont need to send an email to the user // we also dont need to update stripe as thats handled on an ORG level and not a team level. continue; } // create provisional membership await createProvisionalMembership({ input, invitee, }); let sendTo = usernameOrEmail; if (!isEmail(usernameOrEmail)) { sendTo = invitee.email; } // inform user of membership by email if (input.sendEmailInvitation && ctx?.user?.name && team?.name) { const inviteTeamOptions = { joinLink: `${WEBAPP_URL}/auth/login?callbackUrl=/settings/teams`, isCalcomMember: true, }; /** * Here we want to redirect to a different place if onboarding has been completed or not. This prevents the flash of going to teams -> Then to onboarding - also show a different email template. * This only changes if the user is a CAL user and has not completed onboarding and has no password */ if (!invitee.completedOnboarding && !invitee.password && invitee.identityProvider === "CAL") { const token = randomBytes(32).toString("hex"); await prisma.verificationToken.create({ data: { identifier: usernameOrEmail, token, expires: new Date(new Date().setHours(168)), // +1 week }, }); inviteTeamOptions.joinLink = `${WEBAPP_URL}/signup?token=${token}&callbackUrl=/getting-started`; inviteTeamOptions.isCalcomMember = false; } await sendTeamInviteEmail({ language: translation, from: ctx.user.name, to: sendTo, teamName: team.name, ...inviteTeamOptions, isOrg: input.isOrg, }); } } } if (IS_TEAM_BILLING_ENABLED) { if (team.parentId) { await updateQuantitySubscriptionFromStripe(team.parentId); } else { await updateQuantitySubscriptionFromStripe(input.teamId); } } return input; };