diff --git a/packages/features/ee/organizations/components/AddNewTeamsForm.tsx b/packages/features/ee/organizations/components/AddNewTeamsForm.tsx
index 5c51b9b92524d7..a60f5342ed3fef 100644
--- a/packages/features/ee/organizations/components/AddNewTeamsForm.tsx
+++ b/packages/features/ee/organizations/components/AddNewTeamsForm.tsx
@@ -156,7 +156,7 @@ const AddNewTeamsFormChild = ({
orgId: org.id,
moveTeams,
teamNames: fields.map((field) => field.name),
- creationSource: CreationSource.WEBAPP,
+ creationSource: CreationSource.WEBAPP_NEW_ORG,
});
};
diff --git a/packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx b/packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx
index 4c4fb14959dfbd..562b512c4fedbe 100644
--- a/packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx
+++ b/packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx
@@ -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 });
}
}}>
diff --git a/packages/features/ee/platform/components/CreateANewPlatformForm.tsx b/packages/features/ee/platform/components/CreateANewPlatformForm.tsx
index 6f4b4365b67946..19a52448e173ed 100644
--- a/packages/features/ee/platform/components/CreateANewPlatformForm.tsx
+++ b/packages/features/ee/platform/components/CreateANewPlatformForm.tsx
@@ -92,7 +92,7 @@ const CreateANewPlatformFormChild = ({ session }: { session: Ensure
diff --git a/packages/lib/server/repository/organization.test.ts b/packages/lib/server/repository/organization.test.ts
index e23ffda402624d..8249e9de772b25 100644
--- a/packages/lib/server/repository/organization.test.ts
+++ b/packages/lib/server/repository/organization.test.ts
@@ -3,6 +3,7 @@ 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";
@@ -10,6 +11,14 @@ 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: {
@@ -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: "test@test.com",
+ },
+ 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: "test@test.com",
+ severity: WatchlistSeverity.CRITICAL,
+ createdById: 1,
+ },
+ });
+
+ await OrganizationRepository.createWithNonExistentOwner({
+ orgData,
+ owner: {
+ email: "test@test.com",
+ },
+ creationSource: CreationSource.WEBAPP_NEW_ORG,
+ });
+
+ const user = await prismock.user.findFirst({
+ where: {
+ email: "test@test.com",
+ },
+ });
+
+ expect(user).toEqual(
+ expect.objectContaining({
+ locked: true,
+ creationSource: CreationSource.WEBAPP_NEW_ORG,
+ })
+ );
+ });
+});
diff --git a/packages/lib/server/repository/organization.ts b/packages/lib/server/repository/organization.ts
index 809c91df00eb3f..17dfc415d71b94 100644
--- a/packages/lib/server/repository/organization.ts
+++ b/packages/lib/server/repository/organization.ts
@@ -2,6 +2,7 @@ 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";
@@ -9,7 +10,6 @@ import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { createAProfileForAnExistingUser } from "../../createAProfileForAnExistingUser";
import { getParsedTeam } from "./teamUtils";
-import { UserRepository } from "./user";
const orgSelect = {
id: true,
@@ -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,
},
});
diff --git a/packages/lib/server/repository/user.ts b/packages/lib/server/repository/user.ts
index 09d77ccee652bf..2949b4c30aa840 100644
--- a/packages/lib/server/repository/user.ts
+++ b/packages/lib/server/repository/user.ts
@@ -577,19 +577,25 @@ export class UserRepository {
});
}
- static async create(
+ static async create({
+ data,
+ orgData,
+ }: {
data: Omit & {
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);
@@ -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,
diff --git a/packages/lib/server/service/userCreationService.test.ts b/packages/lib/server/service/userCreationService.test.ts
index 39e35fe1de3d8f..ae363fa788110e 100644
--- a/packages/lib/server/service/userCreationService.test.ts
+++ b/packages/lib/server/service/userCreationService.test.ts
@@ -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";
@@ -58,9 +58,12 @@ describe("UserCreationService", () => {
expect(UserRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
- username: "test",
- locked: false,
- organizationId: null,
+ data: {
+ email: "test@example.com",
+ username: "test",
+ creationSource: CreationSource.WEBAPP,
+ locked: false,
+ },
})
);
@@ -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,
+ }),
})
);
@@ -85,6 +96,12 @@ 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 },
});
@@ -92,10 +109,35 @@ describe("UserCreationService", () => {
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,
+ }),
+ })
+ );
+ });
});
diff --git a/packages/lib/server/service/userCreationService.ts b/packages/lib/server/service/userCreationService.ts
index 5056965348511d..8b992b9aed12fd 100644
--- a/packages/lib/server/service/userCreationService.ts
+++ b/packages/lib/server/service/userCreationService.ts
@@ -1,7 +1,12 @@
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
import logger from "@calcom/lib/logger";
-import type { CreationSource, UserPermissionRole, IdentityProvider } from "@calcom/prisma/enums";
+import type {
+ CreationSource,
+ UserPermissionRole,
+ IdentityProvider,
+ MembershipRole,
+} from "@calcom/prisma/enums";
import slugify from "../../slugify";
import { UserRepository } from "../repository/user";
@@ -20,17 +25,22 @@ interface CreateUserInput {
timeFormat?: number;
locale?: string;
avatar?: string;
- organizationId?: number | null;
creationSource: CreationSource;
role?: UserPermissionRole;
emailVerified?: Date;
identityProvider?: IdentityProvider;
}
+interface OrgData {
+ id: number;
+ role: MembershipRole;
+ accepted: boolean;
+}
+
const log = logger.getSubLogger({ prefix: ["[userCreationService]"] });
export class UserCreationService {
- static async createUser({ data }: { data: CreateUserInput }) {
+ static async createUser({ data, orgData }: { data: CreateUserInput; orgData?: OrgData }) {
const { email, password, username } = data;
const shouldLockByDefault = await checkIfEmailIsBlockedInWatchlistController(email);
@@ -38,11 +48,13 @@ export class UserCreationService {
const hashedPassword = password ? await hashPassword(password) : null;
const user = await UserRepository.create({
- ...data,
- username: slugify(username),
- ...(hashedPassword && { hashedPassword }),
- organizationId: data?.organizationId ?? null,
- locked: shouldLockByDefault,
+ data: {
+ ...data,
+ username: slugify(username),
+ ...(hashedPassword && { hashedPassword }),
+ locked: shouldLockByDefault,
+ },
+ ...(orgData ? { orgData } : {}),
});
log.info(`Created user: ${user.id} with locked status of ${user.locked}`);
diff --git a/packages/prisma/migrations/20250210032716_add_new_org_creation_source/migration.sql b/packages/prisma/migrations/20250210032716_add_new_org_creation_source/migration.sql
new file mode 100644
index 00000000000000..13141e7b1ddf06
--- /dev/null
+++ b/packages/prisma/migrations/20250210032716_add_new_org_creation_source/migration.sql
@@ -0,0 +1,10 @@
+-- AlterEnum
+-- This migration adds more than one value to an enum.
+-- With PostgreSQL versions 11 and earlier, this is not possible
+-- in a single migration. This can be worked around by creating
+-- multiple migrations, each migration adding only one value to
+-- the enum.
+
+
+ALTER TYPE "CreationSource" ADD VALUE 'webapp_new_org';
+ALTER TYPE "CreationSource" ADD VALUE 'api_v2_new_org';
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 38d02202f29d07..5127d008dfe6d7 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -47,6 +47,8 @@ enum CreationSource {
API_V2 @map("api_v2")
WEBAPP @map("webapp")
SELF_SERVE_ADMIN @map("self_serve_admin")
+ WEBAPP_NEW_ORG @map("webapp_new_org")
+ API_V2_NEW_ORG @map("api_v2_new_org")
}
model Host {