Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Use UserCreationService when creating new owner for new orgs #19201

Open
wants to merge 9 commits into
base: sys-admin-use-usercreationservice
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ const AddNewTeamsFormChild = ({
orgId: org.id,
moveTeams,
teamNames: fields.map((field) => field.name),
creationSource: CreationSource.WEBAPP,
creationSource: CreationSource.WEBAPP_NEW_ORG,
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ const CreateANewOrganizationFormChild = ({
handleSubmit={(v) => {
if (!createOrganizationMutation.isPending) {
setServerErrorMessage(null);
createOrganizationMutation.mutate({ ...v, creationSource: CreationSource.WEBAPP });
createOrganizationMutation.mutate({ ...v, creationSource: CreationSource.WEBAPP_NEW_ORG });
}
}}>
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const CreateANewPlatformFormChild = ({ session }: { session: Ensure<SessionConte
createOrganizationMutation.mutate({
...v,
slug: `${v.name.toLocaleLowerCase()}-platform-${uuid().substring(0, 20)}`,
creationSource: CreationSource.API_V2,
creationSource: CreationSource.API_V2_NEW_ORG,
});
}
}}>
Expand Down
119 changes: 119 additions & 0 deletions packages/lib/server/repository/organization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@ import prismock from "../../../../tests/libs/__mocks__/prisma";
import { describe, it, expect, beforeEach, vi } from "vitest";

import type { Prisma } from "@calcom/prisma/client";
import { CreationSource, MembershipRole, WatchlistSeverity } from "@calcom/prisma/enums";

import { OrganizationRepository } from "./organization";

vi.mock("./teamUtils", () => ({
getParsedTeam: (org: any) => org,
}));

vi.mock("@calcom/lib/server/i18n", () => {
return {
getTranslation: (key: string) => {
return () => key;
},
};
});

async function createOrganization(
data: Prisma.TeamCreateInput & {
organizationSettings: {
Expand Down Expand Up @@ -151,3 +160,113 @@ describe("Organization.getVerifiedOrganizationByAutoAcceptEmailDomain", () => {
expect(result).toEqual(null);
});
});

describe("Organization.createWithNonExistentOwner", () => {
it("should create organization with non-existent owner", async () => {
const orgData = {
name: "Test Org",
slug: "test-org",
isOrganizationConfigured: true,
isOrganizationAdminReviewed: true,
autoAcceptEmail: "test.com",
seats: 30,
pricePerSeat: 37,
isPlatform: false,
};

const createdOrg = await OrganizationRepository.createWithNonExistentOwner({
orgData,
owner: {
email: "[email protected]",
},
creationSource: CreationSource.WEBAPP_NEW_ORG,
});

expect(createdOrg)
.toHaveProperty("orgOwner")
.toHaveProperty("organization")
.toHaveProperty("ownerProfile");

const { orgOwner, organization } = createdOrg;

const user = await prismock.user.findFirst({
where: {
id: orgOwner.id,
},
include: {
profiles: true,
teams: true,
},
});

expect(user).toEqual(
expect.objectContaining({
locked: false,
creationSource: CreationSource.WEBAPP_NEW_ORG,
})
);

expect(user.profiles).toHaveLength(1);
expect(user.teams).toHaveLength(1);

const userProfile = user.profiles[0];
const userMembership = user.teams[0];

expect(userProfile).toEqual(
expect.objectContaining({
organizationId: organization.id,
})
);

expect(userMembership).toEqual(
expect.objectContaining({
teamId: organization.id,
accepted: true,
role: MembershipRole.OWNER,
})
);
});

it("should lock the organizer if they are on the watchlist", async () => {
const orgData = {
name: "Test Org",
slug: "test-org",
isOrganizationConfigured: true,
isOrganizationAdminReviewed: true,
autoAcceptEmail: "test.com",
seats: 30,
pricePerSeat: 37,
isPlatform: false,
};

await prismock.watchlist.create({
data: {
type: "EMAIL",
value: "[email protected]",
severity: WatchlistSeverity.CRITICAL,
createdById: 1,
},
});

await OrganizationRepository.createWithNonExistentOwner({
orgData,
owner: {
email: "[email protected]",
},
creationSource: CreationSource.WEBAPP_NEW_ORG,
});

const user = await prismock.user.findFirst({
where: {
email: "[email protected]",
},
});

expect(user).toEqual(
expect.objectContaining({
locked: true,
creationSource: CreationSource.WEBAPP_NEW_ORG,
})
);
});
});
20 changes: 8 additions & 12 deletions packages/lib/server/repository/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getO
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { UserCreationService } from "@calcom/lib/server/service/UserCreationService";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import type { CreationSource } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";

import { createAProfileForAnExistingUser } from "../../createAProfileForAnExistingUser";
import { getParsedTeam } from "./teamUtils";
import { UserRepository } from "./user";

const orgSelect = {
id: true,
Expand Down Expand Up @@ -86,20 +86,16 @@ export class OrganizationRepository {
logger.debug("createWithNonExistentOwner", safeStringify({ orgData, owner }));
const organization = await this.create(orgData);
const ownerUsernameInOrg = getOrgUsernameFromEmail(owner.email, orgData.autoAcceptEmail);
const ownerInDb = await UserRepository.create({
email: owner.email,
username: ownerUsernameInOrg,
organizationId: organization.id,
locked: false,
creationSource,
});

await prisma.membership.create({
const ownerInDb = await UserCreationService.createUser({
data: {
userId: ownerInDb.id,
email: owner.email,
username: ownerUsernameInOrg,
creationSource,
},
orgData: {
id: organization.id,
role: MembershipRole.OWNER,
accepted: true,
teamId: organization.id,
},
});

Expand Down
31 changes: 22 additions & 9 deletions packages/lib/server/repository/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,19 +577,25 @@ export class UserRepository {
});
}

static async create(
static async create({
data,
orgData,
}: {
data: Omit<Prisma.UserCreateInput, "password" | "organization" | "movedToProfile"> & {
username: string;
hashedPassword?: string;
organizationId: number | null;
creationSource: CreationSource;
locked: boolean;
}
) {
const organizationIdValue = data.organizationId;
};
orgData?: {
id: number;
role: MembershipRole;
accepted: boolean;
};
}) {
const { email, username, creationSource, locked, ...rest } = data;

logger.info("create user", { email, username, organizationIdValue, locked });
logger.info("create user", { email, username, orgId: orgData?.id, locked });
const t = await getTranslation("en", "common");
const availability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE);

Expand All @@ -615,16 +621,23 @@ export class UserRepository {
},
creationSource,
locked,
...(organizationIdValue
...(orgData
? {
organizationId: organizationIdValue,
organizationId: orgData.id,
profiles: {
create: {
username,
organizationId: organizationIdValue,
organizationId: orgData.id,
uid: ProfileRepository.generateProfileUid(),
},
},
teams: {
create: {
role: orgData.role,
accepted: orgData.accepted,
teamId: orgData.id,
},
},
}
: {}),
...rest,
Expand Down
54 changes: 48 additions & 6 deletions packages/lib/server/service/userCreationService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, test, expect, vi, beforeEach } from "vitest";

import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
import { CreationSource } from "@calcom/prisma/enums";
import { CreationSource, MembershipRole } from "@calcom/prisma/enums";

import { UserRepository } from "../repository/user";
import { UserCreationService } from "./userCreationService";
Expand Down Expand Up @@ -58,9 +58,12 @@ describe("UserCreationService", () => {

expect(UserRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
username: "test",
locked: false,
organizationId: null,
data: {
email: "[email protected]",
username: "test",
creationSource: CreationSource.WEBAPP,
locked: false,
},
})
);

Expand All @@ -70,11 +73,19 @@ describe("UserCreationService", () => {
test("should lock user when email is in watchlist", async () => {
vi.mocked(checkIfEmailIsBlockedInWatchlistController).mockResolvedValue(true);

vi.spyOn(UserRepository, "create").mockResolvedValue({
username: "test",
locked: true,
organizationId: null,
} as any);

const user = await UserCreationService.createUser({ data: mockUserData });

expect(UserRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
locked: true,
data: expect.objectContaining({
locked: true,
}),
})
);

Expand All @@ -85,17 +96,48 @@ describe("UserCreationService", () => {
const mockPassword = "password";
vi.mocked(hashPassword).mockResolvedValue("hashed_password");

vi.spyOn(UserRepository, "create").mockResolvedValue({
username: "test",
locked: true,
organizationId: null,
} as any);

const user = await UserCreationService.createUser({
data: { ...mockUserData, password: mockPassword },
});

expect(hashPassword).toHaveBeenCalledWith(mockPassword);
expect(UserRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
hashedPassword: "hashed_password",
data: expect.objectContaining({
hashedPassword: "hashed_password",
}),
})
);

expect(user).not.toHaveProperty("locked");
});

test("if orgData is passed, user should be created with orgData", async () => {
vi.spyOn(UserRepository, "create").mockResolvedValue({
username: "test",
locked: true,
organizationId: null,
} as any);

const user = await UserCreationService.createUser({
data: mockUserData,
orgData: { id: 1, role: MembershipRole.OWNER, accepted: true },
});

expect(UserRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
orgData: expect.objectContaining({
id: 1,
role: MembershipRole.OWNER,
accepted: true,
}),
})
);
});
});
Loading