cal.pub0.org/packages/trpc/server/routers/viewer/teams/inviteMember.handler.ts

168 lines
5.8 KiB
TypeScript

import { Prisma } from "@prisma/client";
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 { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TInviteMemberInputSchema } from "./inviteMember.schema";
import { isEmail } from "./util";
type InviteMemberOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TInviteMemberInputSchema;
};
export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) => {
if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });
if (input.role === MembershipRole.OWNER && !(await isTeamOwner(ctx.user?.id, input.teamId)))
throw new TRPCError({ code: "UNAUTHORIZED" });
const translation = await getTranslation(input.language ?? "en", "common");
const team = await prisma.team.findFirst({
where: {
id: input.teamId,
},
});
if (!team)
throw new TRPCError({ code: "NOT_FOUND", message: `${input.isOrg ? "Organization" : "Team"} not found` });
const emailsToInvite = Array.isArray(input.usernameOrEmail)
? input.usernameOrEmail
: [input.usernameOrEmail];
emailsToInvite.forEach(async (usernameOrEmail) => {
const invitee = await prisma.user.findFirst({
where: {
OR: [{ username: usernameOrEmail }, { email: usernameOrEmail }],
},
});
if (input.isOrg && invitee) {
throw new TRPCError({
code: "NOT_FOUND",
message: `Email ${usernameOrEmail} already exists, you can't invite existing users.`,
});
}
if (!invitee) {
// liberal email match
if (!isEmail(usernameOrEmail))
throw new TRPCError({
code: "NOT_FOUND",
message: `Invite failed because there is no corresponding user for ${usernameOrEmail}`,
});
// valid email given, create User and add to team
await prisma.user.create({
data: {
email: usernameOrEmail,
invitedTo: input.teamId,
...(input.isOrg && { organizationId: input.teamId }),
teams: {
create: {
teamId: input.teamId,
role: input.role as MembershipRole,
},
},
},
});
const token: string = randomBytes(32).toString("hex");
await prisma.verificationToken.create({
data: {
identifier: usernameOrEmail,
token,
expires: new Date(new Date().setHours(168)), // +1 week
},
});
if (team?.name) {
await sendTeamInviteEmail({
language: translation,
from: ctx.user.name || `${team.name}'s admin`,
to: usernameOrEmail,
teamName: team.name,
joinLink: `${WEBAPP_URL}/signup?token=${token}&callbackUrl=/getting-started`, // we know that the user has not completed onboarding yet, so we can redirect them to the onboarding flow
isCalcomMember: false,
isOrg: input.isOrg,
});
}
} else {
// create provisional membership
try {
await prisma.membership.create({
data: {
teamId: input.teamId,
userId: invitee.id,
role: input.role as MembershipRole,
},
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
// Don't throw an error if the user is already a member of the team when inviting multiple users
if (!Array.isArray(input.usernameOrEmail) && e.code === "P2002") {
throw new TRPCError({
code: "FORBIDDEN",
message: "This user is a member of this team / has a pending invitation.",
});
} else {
console.log(`User ${invitee.id} is already a member of this team.`);
}
} else throw e;
}
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 differnt place if onboarding has been completed or not. This prevents the flash of going to teams -> Then to onboarding - also show a differnt 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) await updateQuantitySubscriptionFromStripe(input.teamId);
return input;
};