168 lines
5.8 KiB
TypeScript
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;
|
|
};
|