feat: Add a fresh signup to matching organization after email verification (#15705)

* Support automatically adding user to organization

* Use Repository

* Fix unit test

* Code Improvements

* Fix type error

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
Hariom Balhara
2024-07-20 12:22:42 +05:30
committed by GitHub
co-authored by Udit Takkar
parent a74522af3d
commit 5becc95fc4
14 changed files with 419 additions and 84 deletions
-1
View File
@@ -43,7 +43,6 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
language: data.language,
teamId: data.teamId,
usernameOrEmail: data.usernameOrEmail,
isOrg: data.isOrg,
});
return { success: true, message: `${data.usernameOrEmail} has been invited.` };
@@ -0,0 +1,82 @@
import { organizationScenarios } from "@calcom/lib/server/repository/__mocks__/organization";
import { describe, it, expect, beforeEach, vi } from "vitest";
import { MembershipRole } from "@calcom/prisma/client";
import { inviteMembersWithNoInviterPermissionCheck } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler";
import { moveUserToMatchingOrg } from "./verify-email";
vi.mock("@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler");
vi.mock("@calcom/lib/server/repository/organization");
describe("moveUserToMatchingOrg", () => {
const email = "test@example.com";
beforeEach(() => {
vi.resetAllMocks();
});
it("should not proceed if no matching organization is found", async () => {
organizationScenarios.OrganizationRepository.findUniqueByMatchingAutoAcceptEmail.fakeNoMatch();
await moveUserToMatchingOrg({ email });
expect(inviteMembersWithNoInviterPermissionCheck).not.toHaveBeenCalled();
});
describe("should invite user to the matching organization", () => {
const argToInviteMembersWithNoInviterPermissionCheck = {
inviterName: null,
language: "en",
invitations: [
{
usernameOrEmail: email,
role: MembershipRole.MEMBER,
},
],
};
it("when organization has a slug and requestedSlug(slug is used)", async () => {
const org = {
id: "org123",
slug: "test-org",
requestedSlug: "requested-test-org",
};
organizationScenarios.OrganizationRepository.findUniqueByMatchingAutoAcceptEmail.fakeReturnOrganization(
org,
{ email }
);
await moveUserToMatchingOrg({ email });
expect(inviteMembersWithNoInviterPermissionCheck).toHaveBeenCalledWith({
...argToInviteMembersWithNoInviterPermissionCheck,
teamId: org.id,
orgSlug: org.slug,
});
});
it("when organization has requestedSlug only", async () => {
const org = {
id: "org123",
slug: null,
requestedSlug: "requested-test-org",
};
organizationScenarios.OrganizationRepository.findUniqueByMatchingAutoAcceptEmail.fakeReturnOrganization(
org,
{ email }
);
await moveUserToMatchingOrg({ email });
expect(inviteMembersWithNoInviterPermissionCheck).toHaveBeenCalledWith({
...argToInviteMembersWithNoInviterPermissionCheck,
teamId: org.id,
orgSlug: org.requestedSlug,
});
});
});
});
+27
View File
@@ -5,8 +5,11 @@ import stripe from "@calcom/app-store/stripepayment/lib/server";
import dayjs from "@calcom/dayjs";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { IS_STRIPE_ENABLED } from "@calcom/lib/constants";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/client";
import { userMetadata } from "@calcom/prisma/zod-utils";
import { inviteMembersWithNoInviterPermissionCheck } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler";
const verifySchema = z.object({
token: z.string(),
@@ -14,6 +17,28 @@ const verifySchema = z.object({
const USER_ALREADY_EXISTING_MESSAGE = "A User already exists with this email";
// TODO: To be unit tested
export async function moveUserToMatchingOrg({ email }: { email: string }) {
const org = await OrganizationRepository.findUniqueByMatchingAutoAcceptEmail({ email });
if (!org) {
return;
}
await inviteMembersWithNoInviterPermissionCheck({
inviterName: null,
teamId: org.id,
language: "en",
invitations: [
{
usernameOrEmail: email,
role: MembershipRole.MEMBER,
},
],
orgSlug: org.slug || org.requestedSlug,
});
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { token } = verifySchema.parse(req.query);
@@ -141,6 +166,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const hasCompletedOnboarding = user.completedOnboarding;
await moveUserToMatchingOrg({ email: user.email });
return res.redirect(`${WEBAPP_URL}/${hasCompletedOnboarding ? "/event-types" : "/getting-started"}`);
}
@@ -143,7 +143,6 @@ export const AddNewTeamMembersForm = ({
language: i18n.language,
role: values.role,
usernameOrEmail: values.emailOrUsername,
isOrg: !!isOrg,
},
{
onSuccess: async (data) => {
@@ -68,7 +68,6 @@ export function InviteMemberModal(props: Props) {
language: i18n.language,
role: values.role,
usernameOrEmail: values.emailOrUsername,
isOrg: true,
});
}}
/>
@@ -0,0 +1,36 @@
import { vi, beforeEach } from "vitest";
import { mockReset, mockDeep } from "vitest-mock-extended";
import type * as organization from "@calcom/lib/server/repository/organization";
vi.mock("@calcom/lib/server/repository/organization", () => organizationMock);
type OrganizationModule = typeof organization;
beforeEach(() => {
mockReset(organizationMock);
});
const organizationMock = mockDeep<OrganizationModule>();
const OrganizationRepository = organizationMock.OrganizationRepository;
export const organizationScenarios = {
OrganizationRepository: {
findUniqueByMatchingAutoAcceptEmail: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fakeReturnOrganization: (org: any, forInput: any) => {
OrganizationRepository.findUniqueByMatchingAutoAcceptEmail.mockImplementation((arg) => {
if (forInput.email === arg.email) {
return org;
}
const errorMsg = "Mock Error-fakeReturnOrganization: Unhandled input";
console.log(errorMsg, { arg, forInput });
throw new Error(errorMsg);
});
},
fakeNoMatch: () => {
OrganizationRepository.findUniqueByMatchingAutoAcceptEmail.mockResolvedValue(null);
},
},
} satisfies Partial<Record<keyof OrganizationModule["OrganizationRepository"], unknown>>,
} satisfies Partial<Record<keyof OrganizationModule, unknown>>;
export default organizationMock;
@@ -0,0 +1,105 @@
import prismock from "../../../../tests/libs/__mocks__/prisma";
import { describe, it, expect, beforeEach, vi } from "vitest";
import { OrganizationRepository } from "./organization";
vi.mock("./teamUtils", () => ({
getParsedTeam: (org: any) => org,
}));
describe("Organization.findUniqueByMatchingAutoAcceptEmail", () => {
beforeEach(async () => {
vi.resetAllMocks();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await prismock.reset();
});
it("should return null if no organization matches the email domain", async () => {
const result = await OrganizationRepository.findUniqueByMatchingAutoAcceptEmail({
email: "test@example.com",
});
expect(result).toBeNull();
});
it("should throw an error if multiple organizations match the email domain", async () => {
await createOrganization({ name: "Test Org 1", orgAutoAcceptEmail: "example.com" });
await createOrganization({ name: "Test Org 2", orgAutoAcceptEmail: "example.com" });
await expect(
OrganizationRepository.findUniqueByMatchingAutoAcceptEmail({ email: "test@example.com" })
).rejects.toThrow("Multiple organizations found with the same auto accept email domain");
});
it("should return the parsed organization if a single match is found", async () => {
const organization = await createOrganization({ name: "Test Org", orgAutoAcceptEmail: "example.com" });
const result = await OrganizationRepository.findUniqueByMatchingAutoAcceptEmail({
email: "test@example.com",
});
expect(result).toEqual(organization);
});
it("should not confuse a team with organization", async () => {
await createTeam({ name: "Test Team", orgAutoAcceptEmail: "example.com" });
const result = await OrganizationRepository.findUniqueByMatchingAutoAcceptEmail({
email: "test@example.com",
});
expect(result).toEqual(null);
});
it("should correctly match orgAutoAcceptEmail", async () => {
await createOrganization({ name: "Test Org", orgAutoAcceptEmail: "noexample.com" });
const result = await OrganizationRepository.findUniqueByMatchingAutoAcceptEmail({
email: "test@example.com",
});
expect(result).toEqual(null);
});
});
async function createOrganization({
name = "Test Org",
orgAutoAcceptEmail,
}: {
name: string;
orgAutoAcceptEmail: string;
}) {
return await prismock.team.create({
data: {
name,
isOrganization: true,
organizationSettings: {
create: {
orgAutoAcceptEmail,
},
},
},
});
}
async function createTeam({
name = "Test Team",
orgAutoAcceptEmail,
}: {
name: string;
orgAutoAcceptEmail: string;
}) {
return await prismock.team.create({
data: {
name,
isOrganization: false,
organizationSettings: {
create: {
orgAutoAcceptEmail,
},
},
},
});
}
@@ -6,6 +6,7 @@ import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { createAProfileForAnExistingUser } from "../../createAProfileForAnExistingUser";
import { getParsedTeam } from "./teamUtils";
import { UserRepository } from "./user";
const orgSelect = {
@@ -163,4 +164,25 @@ export class OrganizationRepository {
},
});
}
static async findUniqueByMatchingAutoAcceptEmail({ email }: { email: string }) {
const emailDomain = email.split("@").at(-1);
const orgs = await prisma.team.findMany({
where: {
isOrganization: true,
organizationSettings: {
orgAutoAcceptEmail: emailDomain,
},
},
});
if (orgs.length > 1) {
// Detect and fail just in case this situation arises. We should really identify the problem in this case and fix the data.
throw new Error("Multiple organizations found with the same auto accept email domain");
}
const org = orgs[0];
if (!org) {
return null;
}
return getParsedTeam(org);
}
}
@@ -240,7 +240,6 @@ async function moveTeam({
teamId: org.id,
language: "en",
usernameOrEmail: invitableMembers,
isOrg: true,
},
});
}
@@ -33,7 +33,8 @@ export const inviteMemberutilsScenarios = {
if (forInput.teamId === teamId) {
return fakedVal;
}
throw new Error("Mock Error: Unhandled input");
console.log("Mock Error: Unhandled input", { teamId });
throw new Error(`Mock Error: Unhandled input. teamId: ${teamId}`);
});
return fakedVal;
},
@@ -160,12 +160,17 @@ describe("inviteMemberHandler", () => {
},
};
const inviter = {
name: loggedInUser.name,
};
expect(inviteMemberUtilsMock.handleNewUsersInvites).toHaveBeenCalledWith({
invitationsForNewUsers: allExpectedInvitations,
team: retValueOfGetTeamOrThrowError,
orgConnectInfoByUsernameOrEmail: expectedConnectionInfoMap,
input,
inviter: loggedInUser,
teamId: input.teamId,
language: input.language,
inviter,
autoAcceptEmailDomain: null,
});
@@ -185,7 +190,7 @@ describe("inviteMemberHandler", () => {
]);
// Assert the result
expect(result).toEqual({
...input,
usernameOrEmail: input.usernameOrEmail,
numUsersInvited: 2,
});
});
@@ -210,7 +215,6 @@ describe("inviteMemberHandler", () => {
const input = {
teamId: 1,
role: MembershipRole.MEMBER,
isOrg: false,
language: "en",
usernameOrEmail: usersToBeInvited.map((u) => u.email),
};
@@ -219,6 +223,7 @@ describe("inviteMemberHandler", () => {
id: input.teamId,
name: "Team 1",
parent: null,
isOrganization: false,
};
const retValueOfGetTeamOrThrowError = inviteMemberUtilsScenarios.getTeamOrThrow.fakeReturnTeam(team, {
@@ -266,30 +271,37 @@ describe("inviteMemberHandler", () => {
autoAccept: false,
},
};
const inviter = {
name: loggedInUser.name,
};
expect(inviteMemberUtilsMock.handleNewUsersInvites).toHaveBeenCalledWith({
invitationsForNewUsers: allExpectedInvitations.slice(1),
team: retValueOfGetTeamOrThrowError,
orgConnectInfoByUsernameOrEmail: expectedConnectionInfoMap,
input,
inviter: loggedInUser,
teamId: input.teamId,
language: input.language,
inviter,
autoAcceptEmailDomain: null,
isOrg: false,
});
expect(paymentsMock.updateQuantitySubscriptionFromStripe).toHaveBeenCalledWith(input.teamId);
expect(inviteMemberUtilsMock.handleExistingUsersInvites).toHaveBeenCalledWith({
invitableExistingUsers: retValueOfFindUsersWithInviteStatus,
input: input,
inviter: loggedInUser,
teamId: input.teamId,
language: input.language,
inviter,
orgConnectInfoByUsernameOrEmail: expectedConnectionInfoMap,
orgSlug: null,
team: retValueOfGetTeamOrThrowError,
isOrg: false,
});
// Assert the result
expect(result).toEqual({
...input,
usernameOrEmail: input.usernameOrEmail,
numUsersInvited: 2,
});
});
@@ -121,34 +121,31 @@ function buildInvitationsFromInput({
});
}
export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) => {
const myLog = log.getSubLogger({ prefix: ["inviteMemberHandler"] });
const translation = await getTranslation(input.language ?? "en", "common");
await checkRateLimitAndThrowError({
identifier: `invitedBy:${ctx.user.id}`,
});
const invitations = buildInvitationsFromInput({
usernameOrEmail: input.usernameOrEmail,
roleForAllInvitees: input.role,
});
const team = await getTeamOrThrow(input.teamId);
type TargetTeam =
| {
teamId: number;
}
| {
team: TeamWithParent;
};
export const inviteMembersWithNoInviterPermissionCheck = async (
data: {
// TODO: Remove `input` and instead pass the required fields directly
language: string;
inviterName: string | null;
orgSlug: string | null;
invitations: {
usernameOrEmail: string;
role: MembershipRole;
}[];
} & TargetTeam
) => {
const { inviterName, orgSlug, invitations, language } = data;
const myLog = log.getSubLogger({ prefix: ["inviteMembers"] });
const translation = await getTranslation(language ?? "en", "common");
const team = "team" in data ? data.team : await getTeamOrThrow(data.teamId);
const isTeamAnOrg = team.isOrganization;
const isAddingNewOwner = !!invitations.find((invitation) => invitation.role === MembershipRole.OWNER);
const inviter = ctx.user;
const inviterOrg = inviter.organization;
if (isTeamAnOrg) {
await throwIfInviterCantAddOwnerToOrg();
}
await ensureAtleastAdminPermissions({
userId: ctx.user.id,
teamId: inviterOrg.id && inviterOrg.isOrgAdmin ? inviterOrg.id : input.teamId,
isOrg: isTeamAnOrg,
});
const uniqueInvitations = await getUniqueInvitationsOrThrowIfEmpty(invitations);
const beSilentAboutErrors = shouldBeSilentAboutErrors(uniqueInvitations);
@@ -179,13 +176,17 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) =
uniqueInvitations,
});
const inviter = { name: inviterName };
if (invitationsForNewUsers.length) {
await handleNewUsersInvites({
invitationsForNewUsers,
team,
orgConnectInfoByUsernameOrEmail,
input,
inviter: ctx.user,
teamId: team.id,
language,
isOrg: isTeamAnOrg,
inviter,
autoAcceptEmailDomain: orgState.autoAcceptEmailDomain,
});
}
@@ -207,36 +208,76 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) =
);
if (invitableExistingUsers.length) {
const organization = ctx.user.profile.organization;
const orgSlug = organization ? organization.slug || organization.requestedSlug : null;
await handleExistingUsersInvites({
invitableExistingUsers,
team,
orgConnectInfoByUsernameOrEmail,
input,
inviter: ctx.user,
teamId: team.id,
language,
isOrg: isTeamAnOrg,
inviter,
orgSlug,
});
}
if (IS_TEAM_BILLING_ENABLED) {
await updateQuantitySubscriptionFromStripe(team.parentId ?? input.teamId);
await updateQuantitySubscriptionFromStripe(team.parentId ?? team.id);
}
return {
...input,
// TODO: Better rename it to invitations only maybe?
usernameOrEmail:
invitations.length == 1
? invitations[0].usernameOrEmail
: invitations.map((invitation) => invitation.usernameOrEmail),
numUsersInvited: invitableExistingUsers.length + invitationsForNewUsers.length,
};
};
const inviteMembers = async ({ ctx, input }: InviteMemberOptions) => {
const { user: inviter } = ctx;
const inviterOrg = inviter.organization;
const team = await getTeamOrThrow(input.teamId);
const isTeamAnOrg = team.isOrganization;
const invitations = buildInvitationsFromInput({
usernameOrEmail: input.usernameOrEmail,
roleForAllInvitees: input.role,
});
const isAddingNewOwner = !!invitations.find((invitation) => invitation.role === MembershipRole.OWNER);
if (isTeamAnOrg) {
await throwIfInviterCantAddOwnerToOrg();
}
await ensureAtleastAdminPermissions({
userId: inviter.id,
teamId: inviterOrg.id && inviterOrg.isOrgAdmin ? inviterOrg.id : input.teamId,
isOrg: isTeamAnOrg,
});
const organization = inviter.profile.organization;
const orgSlug = organization ? organization.slug || organization.requestedSlug : null;
const result = await inviteMembersWithNoInviterPermissionCheck({
inviterName: inviter.name,
team,
language: input.language,
orgSlug,
invitations,
});
return result;
async function throwIfInviterCantAddOwnerToOrg() {
const isInviterOrgOwner = await isOrganisationOwner(ctx.user.id, input.teamId);
const isInviterOrgOwner = await isOrganisationOwner(inviter.id, input.teamId);
if (isAddingNewOwner && !isInviterOrgOwner) throw new TRPCError({ code: "UNAUTHORIZED" });
}
};
async function handleSubscriptionUpdates(teamId: number) {
if (!IS_TEAM_BILLING_ENABLED) return;
await updateQuantitySubscriptionFromStripe(teamId);
export default async function inviteMemberHandler({ ctx, input }: InviteMemberOptions) {
const { user: inviter } = ctx;
await checkRateLimitAndThrowError({
identifier: `invitedBy:${inviter.id}`,
});
return await inviteMembers({ ctx, input });
}
export default inviteMemberHandler;
@@ -55,7 +55,6 @@ export const ZInviteMemberInputSchema = z.object({
),
role: z.nativeEnum(MembershipRole).optional(),
language: z.string(),
isOrg: z.boolean().default(false),
});
export type TInviteMemberInputSchema = z.infer<typeof ZInviteMemberInputSchema>;
@@ -25,7 +25,7 @@ import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
import { isEmail } from "../util";
import type { InviteMemberOptions, TeamWithParent } from "./types";
import type { TeamWithParent } from "./types";
const log = logger.getSubLogger({ prefix: ["inviteMember.utils"] });
export type Invitee = Pick<
@@ -44,8 +44,11 @@ export type Invitation = {
role: MembershipRole;
};
type ExistingUserWithInviteStatus = Awaited<ReturnType<typeof findUsersWithInviteStatus>>[number];
type ExistingUserWithInviteStatusAndProfile = ExistingUserWithInviteStatus & {
type InvitableExistingUser = UserWithMembership & {
newRole: MembershipRole;
};
type InvitableExistingUserWithProfile = InvitableExistingUser & {
profile: {
username: string;
} | null;
@@ -361,19 +364,21 @@ export async function createNewUsersConnectToOrgIfExists({
}
export async function createMemberships({
input,
teamId,
language,
invitees,
parentId,
accepted,
}: {
input: Omit<InviteMemberOptions["input"], "usernameOrEmail">;
invitees: (ExistingUserWithInviteStatus & {
teamId: number;
language: string;
invitees: (InvitableExistingUser & {
needToCreateOrgMembership: boolean | null;
})[];
parentId: number | null;
accepted: boolean;
}) {
log.debug("Creating memberships for", safeStringify({ input, invitees, parentId, accepted }));
log.debug("Creating memberships for", safeStringify({ teamId, language, invitees, parentId, accepted }));
try {
await prisma.membership.createMany({
data: invitees.flatMap((invitee) => {
@@ -381,7 +386,7 @@ export async function createMemberships({
const data = [];
// membership for the team
data.push({
teamId: input.teamId,
teamId,
userId: invitee.id,
accepted,
role:
@@ -404,7 +409,7 @@ export async function createMemberships({
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
logger.error("Failed to create memberships", input.teamId);
logger.error("Failed to create memberships", teamId);
} else {
throw e;
}
@@ -558,7 +563,7 @@ export const groupUsersByJoinability = ({
connectionInfoMap,
}: {
team: TeamWithParent;
existingUsersWithMemberships: ExistingUserWithInviteStatusAndProfile[];
existingUsersWithMemberships: InvitableExistingUserWithProfile[];
connectionInfoMap: Record<string, ReturnType<typeof getOrgConnectionInfo>>;
}) => {
const usersToAutoJoin = [];
@@ -608,7 +613,7 @@ export const sendExistingUserTeamInviteEmails = async ({
}: {
language: TFunction;
isAutoJoin: boolean;
existingUsersWithMemberships: Omit<ExistingUserWithInviteStatusAndProfile, "canBeInvited" | "newRole">[];
existingUsersWithMemberships: Omit<InvitableExistingUserWithProfile, "canBeInvited" | "newRole">[];
currentUserTeamName?: string;
currentUserParentTeamName: string | undefined;
currentUserName?: string | null;
@@ -682,7 +687,6 @@ export const sendExistingUserTeamInviteEmails = async ({
type inviteMemberHandlerInput = {
teamId: number;
role?: "ADMIN" | "MEMBER" | "OWNER";
isOrg: boolean;
language: string;
};
@@ -690,20 +694,24 @@ export async function handleExistingUsersInvites({
invitableExistingUsers,
team,
orgConnectInfoByUsernameOrEmail,
input,
teamId,
language,
inviter,
orgSlug,
isOrg,
}: {
invitableExistingUsers: Awaited<ReturnType<typeof findUsersWithInviteStatus>>;
invitableExistingUsers: InvitableExistingUser[];
team: TeamWithParent;
orgConnectInfoByUsernameOrEmail: Record<string, { orgId: number | undefined; autoAccept: boolean }>;
input: inviteMemberHandlerInput;
teamId: number;
language: string;
inviter: {
name: string | null;
};
isOrg: boolean;
orgSlug: string | null;
}) {
const translation = await getTranslation(input.language ?? "en", "common");
const translation = await getTranslation(language, "common");
if (!team.isOrganization) {
const [autoJoinUsers, regularUsers] = groupUsersByJoinability({
existingUsersWithMemberships: invitableExistingUsers.map((u) => {
@@ -727,7 +735,8 @@ export async function handleExistingUsersInvites({
// invited users can autojoin, create their memberships in org
if (autoJoinUsers.length) {
await createMemberships({
input,
teamId,
language,
invitees: autoJoinUsers,
parentId: team.parentId,
accepted: true,
@@ -744,7 +753,7 @@ export async function handleExistingUsersInvites({
currentUserTeamName: team?.name,
existingUsersWithMemberships: autoJoinUsers,
language: translation,
isOrg: input.isOrg,
isOrg: isOrg,
teamId: team.id,
isAutoJoin: true,
currentUserParentTeamName: team?.parent?.name,
@@ -755,7 +764,8 @@ export async function handleExistingUsersInvites({
// invited users cannot autojoin, create provisional memberships and send email
if (regularUsers.length) {
await createMemberships({
input,
teamId,
language,
invitees: regularUsers,
parentId: team.parentId,
accepted: false,
@@ -765,7 +775,7 @@ export async function handleExistingUsersInvites({
currentUserTeamName: team?.name,
existingUsersWithMemberships: regularUsers,
language: translation,
isOrg: input.isOrg,
isOrg: isOrg,
teamId: team.id,
isAutoJoin: false,
currentUserParentTeamName: team?.parent?.name,
@@ -846,7 +856,7 @@ export async function handleExistingUsersInvites({
currentUserTeamName: team?.name,
existingUsersWithMemberships: autoJoinUsers,
language: translation,
isOrg: input.isOrg,
isOrg,
teamId: team.id,
isAutoJoin: true,
currentUserParentTeamName: team?.parent?.name,
@@ -859,7 +869,7 @@ export async function handleExistingUsersInvites({
currentUserTeamName: team?.name,
existingUsersWithMemberships: regularUsers,
language: translation,
isOrg: input.isOrg,
isOrg,
teamId: team.id,
isAutoJoin: false,
currentUserParentTeamName: team?.parent?.name,
@@ -872,25 +882,29 @@ export async function handleNewUsersInvites({
invitationsForNewUsers,
team,
orgConnectInfoByUsernameOrEmail,
input,
teamId,
language,
isOrg,
autoAcceptEmailDomain,
inviter,
}: {
invitationsForNewUsers: Invitation[];
input: inviteMemberHandlerInput;
teamId: number;
language: string;
orgConnectInfoByUsernameOrEmail: Record<string, { orgId: number | undefined; autoAccept: boolean }>;
autoAcceptEmailDomain: string | null;
team: TeamWithParent;
inviter: {
name: string | null;
};
isOrg: boolean;
}) {
const translation = await getTranslation(input.language ?? "en", "common");
const translation = await getTranslation(language, "common");
await createNewUsersConnectToOrgIfExists({
invitations: invitationsForNewUsers,
isOrg: input.isOrg,
teamId: input.teamId,
isOrg,
teamId: teamId,
orgConnectInfoByUsernameOrEmail,
autoAcceptEmailDomain: autoAcceptEmailDomain,
parentId: team.parentId,
@@ -905,8 +919,8 @@ export async function handleNewUsersInvites({
},
translation,
inviterName: inviter.name ?? "",
teamId: input.teamId,
isOrg: input.isOrg,
teamId,
isOrg,
});
});
await sendEmails(sendVerifyEmailsPromises);