diff --git a/apps/api/v2/src/lib/services/organization-membership.service.ts b/apps/api/v2/src/lib/services/organization-membership.service.ts new file mode 100644 index 0000000000..59be6027c1 --- /dev/null +++ b/apps/api/v2/src/lib/services/organization-membership.service.ts @@ -0,0 +1,12 @@ +import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; +import { Injectable } from "@nestjs/common"; + +import { OrganizationMembershipService as BaseOrganizationMembershipService } from "@calcom/platform-libraries/organizations"; + +@Injectable() +export class OrganizationMembershipService extends BaseOrganizationMembershipService { + constructor(organizationsRepository: OrganizationsRepository) { + super({ organizationRepository: organizationsRepository }); + } +} + diff --git a/apps/api/v2/src/modules/auth/guards/organizations/is-admin-api-enabled.guard.ts b/apps/api/v2/src/modules/auth/guards/organizations/is-admin-api-enabled.guard.ts index 0f7e2ddb83..410ce2902f 100644 --- a/apps/api/v2/src/modules/auth/guards/organizations/is-admin-api-enabled.guard.ts +++ b/apps/api/v2/src/modules/auth/guards/organizations/is-admin-api-enabled.guard.ts @@ -54,7 +54,7 @@ export class IsAdminAPIEnabledGuard implements CanActivate { } } - const org = await this.organizationsRepository.findById(Number(organizationId)); + const org = await this.organizationsRepository.findById({ id: Number(organizationId) }); if (org?.isOrganization && !org?.isPlatform) { const adminAPIAccessIsEnabledInOrg = await this.organizationsRepository.fetchOrgAdminApiStatus( diff --git a/apps/api/v2/src/modules/auth/guards/organizations/is-org.guard.ts b/apps/api/v2/src/modules/auth/guards/organizations/is-org.guard.ts index 31d287bfdf..df144a5d88 100644 --- a/apps/api/v2/src/modules/auth/guards/organizations/is-org.guard.ts +++ b/apps/api/v2/src/modules/auth/guards/organizations/is-org.guard.ts @@ -55,7 +55,7 @@ export class IsOrgGuard implements CanActivate { } } - const org = await this.organizationsRepository.findById(Number(organizationId)); + const org = await this.organizationsRepository.findById({ id: Number(organizationId) }); if (org?.isOrganization) { canAccess = true; diff --git a/apps/api/v2/src/modules/auth/guards/organizations/is-webhook-in-org.guard.ts b/apps/api/v2/src/modules/auth/guards/organizations/is-webhook-in-org.guard.ts index 1dc6e1dc95..7fe3e03e54 100644 --- a/apps/api/v2/src/modules/auth/guards/organizations/is-webhook-in-org.guard.ts +++ b/apps/api/v2/src/modules/auth/guards/organizations/is-webhook-in-org.guard.ts @@ -43,7 +43,7 @@ export class IsWebhookInOrg implements CanActivate { } } - const org = await this.organizationsRepository.findById(Number(organizationId)); + const org = await this.organizationsRepository.findById({ id: Number(organizationId) }); if (org?.isOrganization) { const isWebhookInOrg = await this.organizationsWebhooksRepository.findWebhook( diff --git a/apps/api/v2/src/modules/organizations/index/organizations.repository.ts b/apps/api/v2/src/modules/organizations/index/organizations.repository.ts index 00035ca40f..ba9b0eecde 100644 --- a/apps/api/v2/src/modules/organizations/index/organizations.repository.ts +++ b/apps/api/v2/src/modules/organizations/index/organizations.repository.ts @@ -4,23 +4,17 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { StripeService } from "@/modules/stripe/stripe.service"; import { Injectable } from "@nestjs/common"; +import { OrganizationRepository } from "@calcom/platform-libraries/organizations"; import { Prisma } from "@calcom/prisma/client"; @Injectable() -export class OrganizationsRepository { +export class OrganizationsRepository extends OrganizationRepository { constructor( private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService, private readonly stripeService: StripeService - ) {} - - async findById(organizationId: number) { - return this.dbRead.prisma.team.findUnique({ - where: { - id: organizationId, - isOrganization: true, - }, - }); + ) { + super({ prismaClient: dbWrite.prisma }); } async findByIds(organizationIds: number[]) { diff --git a/apps/api/v2/src/modules/organizations/index/organizations.service.ts b/apps/api/v2/src/modules/organizations/index/organizations.service.ts index 07c2205a0d..b6e63c4927 100644 --- a/apps/api/v2/src/modules/organizations/index/organizations.service.ts +++ b/apps/api/v2/src/modules/organizations/index/organizations.service.ts @@ -6,7 +6,7 @@ export class OrganizationsService { constructor(private readonly organizationsRepository: OrganizationsRepository) {} async isPlatform(organizationId: number) { - const organization = await this.organizationsRepository.findById(organizationId); + const organization = await this.organizationsRepository.findById({ id: organizationId }); return organization?.isPlatform; } } diff --git a/apps/api/v2/src/modules/organizations/organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations.module.ts index 58b5347796..c046434553 100644 --- a/apps/api/v2/src/modules/organizations/organizations.module.ts +++ b/apps/api/v2/src/modules/organizations/organizations.module.ts @@ -13,6 +13,7 @@ import { ZoomVideoService } from "@/modules/conferencing/services/zoom-video.ser import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { EmailModule } from "@/modules/email/email.module"; import { EmailService } from "@/modules/email/email.service"; +import { OrganizationMembershipService } from "@/lib/services/organization-membership.service"; import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository"; @@ -108,6 +109,7 @@ import { Module } from "@nestjs/common"; ], providers: [ OrganizationsRepository, + OrganizationMembershipService, OrganizationsTeamsRepository, OrganizationsService, OrganizationsTeamsService, @@ -200,4 +202,4 @@ import { Module } from "@nestjs/common"; OrganizationsEventTypesPrivateLinksController, ], }) -export class OrganizationsModule {} +export class OrganizationsModule { } diff --git a/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations.service.ts b/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations.service.ts index 303563d1ca..f423fb35ac 100644 --- a/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations.service.ts +++ b/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations.service.ts @@ -103,12 +103,12 @@ export class ManagedOrganizationsService { } private async isManagerOrganizationPlatform(managerOrganizationId: number) { - const organization = await this.organizationsRepository.findById(managerOrganizationId); + const organization = await this.organizationsRepository.findById({ id: managerOrganizationId }); return !!organization?.isPlatform; } async getManagedOrganization(managedOrganizationId: number) { - const organization = await this.organizationsRepository.findById(managedOrganizationId); + const organization = await this.organizationsRepository.findById({ id: managedOrganizationId }); if (!organization) { throw new NotFoundException(`Managed organization with id=${managedOrganizationId} does not exist.`); } diff --git a/apps/api/v2/src/modules/organizations/teams/memberships/e2e/organizations-teams-memberships.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/memberships/e2e/organizations-teams-memberships.controller.e2e-spec.ts index c2616685bf..315e8a5f08 100644 --- a/apps/api/v2/src/modules/organizations/teams/memberships/e2e/organizations-teams-memberships.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/teams/memberships/e2e/organizations-teams-memberships.controller.e2e-spec.ts @@ -418,6 +418,209 @@ describe("Organizations Teams Memberships Endpoints", () => { .expect(404); }); + // Auto-accept tests + describe("auto-accept based on email domain", () => { + let orgWithAutoAccept: Team; + let subteamWithAutoAccept: Team; + let userWithMatchingEmail: User; + let userWithUppercaseEmail: User; + let userWithMatchingEmailForOverride: User; + let userWithNonMatchingEmail: User; + + beforeAll(async () => { + // Create org with auto-accept settings + orgWithAutoAccept = await organizationsRepositoryFixture.create({ + name: `auto-accept-org-${randomString()}`, + isOrganization: true, + }); + + // Update organizationSettings with orgAutoAcceptEmail + await organizationsRepositoryFixture.updateSettings(orgWithAutoAccept.id, { + orgAutoAcceptEmail: "acme.com", + isOrganizationVerified: true, + isOrganizationConfigured: true, + }); + + // Create subteam + subteamWithAutoAccept = await teamsRepositoryFixture.create({ + name: `auto-accept-subteam-${randomString()}`, + isOrganization: false, + parent: { connect: { id: orgWithAutoAccept.id } }, + }); + + // Create event type with assignAllTeamMembers + await eventTypesRepositoryFixture.createTeamEventType({ + schedulingType: "COLLECTIVE", + team: { connect: { id: subteamWithAutoAccept.id } }, + title: "Auto Accept Event Type", + slug: "auto-accept-event-type", + length: 30, + assignAllTeamMembers: true, + bookingFields: [], + locations: [], + }); + + // Create users + userWithMatchingEmail = await userRepositoryFixture.create({ + email: `alice@acme.com`, + username: `alice-${randomString()}`, + }); + + userWithUppercaseEmail = await userRepositoryFixture.create({ + email: `bob@ACME.COM`, + username: `bob-${randomString()}`, + }); + + userWithMatchingEmailForOverride = await userRepositoryFixture.create({ + email: `david@acme.com`, + username: `david-${randomString()}`, + }); + + userWithNonMatchingEmail = await userRepositoryFixture.create({ + email: `charlie@external.com`, + username: `charlie-${randomString()}`, + }); + + // Add users to org + await membershipsRepositoryFixture.create({ + role: "MEMBER", + accepted: true, + user: { connect: { id: userWithMatchingEmail.id } }, + team: { connect: { id: orgWithAutoAccept.id } }, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + accepted: true, + user: { connect: { id: userWithUppercaseEmail.id } }, + team: { connect: { id: orgWithAutoAccept.id } }, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + accepted: true, + user: { connect: { id: userWithMatchingEmailForOverride.id } }, + team: { connect: { id: orgWithAutoAccept.id } }, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + accepted: true, + user: { connect: { id: userWithNonMatchingEmail.id } }, + team: { connect: { id: orgWithAutoAccept.id } }, + }); + + // Create profiles for users + await profileRepositoryFixture.create({ + uid: `usr-${userWithMatchingEmail.id}`, + username: userWithMatchingEmail.username || `user-${userWithMatchingEmail.id}`, + organization: { connect: { id: orgWithAutoAccept.id } }, + user: { connect: { id: userWithMatchingEmail.id } }, + }); + + await profileRepositoryFixture.create({ + uid: `usr-${userWithUppercaseEmail.id}`, + username: userWithUppercaseEmail.username || `user-${userWithUppercaseEmail.id}`, + organization: { connect: { id: orgWithAutoAccept.id } }, + user: { connect: { id: userWithUppercaseEmail.id } }, + }); + + await profileRepositoryFixture.create({ + uid: `usr-${userWithMatchingEmailForOverride.id}`, + username: userWithMatchingEmailForOverride.username || `user-${userWithMatchingEmailForOverride.id}`, + organization: { connect: { id: orgWithAutoAccept.id } }, + user: { connect: { id: userWithMatchingEmailForOverride.id } }, + }); + + await profileRepositoryFixture.create({ + uid: `usr-${userWithNonMatchingEmail.id}`, + username: userWithNonMatchingEmail.username || `user-${userWithNonMatchingEmail.id}`, + organization: { connect: { id: orgWithAutoAccept.id } }, + user: { connect: { id: userWithNonMatchingEmail.id } }, + }); + + // Make user an admin of the org for API access + await membershipsRepositoryFixture.create({ + role: "ADMIN", + accepted: true, + user: { connect: { id: user.id } }, + team: { connect: { id: orgWithAutoAccept.id } }, + }); + }); + + it("should auto-accept when email matches orgAutoAcceptEmail", async () => { + const response = await request(app.getHttpServer()) + .post(`/v2/organizations/${orgWithAutoAccept.id}/teams/${subteamWithAutoAccept.id}/memberships`) + .send({ + userId: userWithMatchingEmail.id, + role: "MEMBER", + } satisfies CreateOrgTeamMembershipDto) + .expect(201); + + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.data.accepted).toBe(true); + + // Verify EventTypes assignment + const eventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes( + subteamWithAutoAccept.id + ); + const eventTypeWithAssignAll = eventTypes.find((et) => et.assignAllTeamMembers); + expect(eventTypeWithAssignAll).toBeTruthy(); + const userIsHost = eventTypeWithAssignAll?.hosts.some((h) => h.userId === userWithMatchingEmail.id); + expect(userIsHost).toBe(true); + }); + + it("should handle case-insensitive email domain matching", async () => { + // User with email="bob@ACME.COM" should match orgAutoAcceptEmail="acme.com" + const response = await request(app.getHttpServer()) + .post(`/v2/organizations/${orgWithAutoAccept.id}/teams/${subteamWithAutoAccept.id}/memberships`) + .send({ + userId: userWithUppercaseEmail.id, + role: "MEMBER", + } satisfies CreateOrgTeamMembershipDto) + .expect(201); + + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.data.accepted).toBe(true); + }); + + it("should ALWAYS auto-accept when email matches, even if accepted:false", async () => { + const response = await request(app.getHttpServer()) + .post(`/v2/organizations/${orgWithAutoAccept.id}/teams/${subteamWithAutoAccept.id}/memberships`) + .send({ + userId: userWithMatchingEmailForOverride.id, + role: "MEMBER", + accepted: false, + } satisfies CreateOrgTeamMembershipDto) + .expect(201); + + const responseBody: ApiSuccessResponse = response.body; + // Should override to true because email matches + expect(responseBody.data.accepted).toBe(true); + }); + + it("should NOT auto-accept when email does not match orgAutoAcceptEmail", async () => { + const response = await request(app.getHttpServer()) + .post(`/v2/organizations/${orgWithAutoAccept.id}/teams/${subteamWithAutoAccept.id}/memberships`) + .send({ + userId: userWithNonMatchingEmail.id, + role: "MEMBER", + } satisfies CreateOrgTeamMembershipDto) + .expect(201); + + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.data.accepted).toBe(false); + }); + + afterAll(async () => { + await userRepositoryFixture.deleteByEmail(userWithMatchingEmail.email); + await userRepositoryFixture.deleteByEmail(userWithUppercaseEmail.email); + await userRepositoryFixture.deleteByEmail(userWithMatchingEmailForOverride.email); + await userRepositoryFixture.deleteByEmail(userWithNonMatchingEmail.email); + await organizationsRepositoryFixture.delete(orgWithAutoAccept.id); + }); + }); + afterAll(async () => { await userRepositoryFixture.deleteByEmail(user.email); await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email); diff --git a/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.controller.ts b/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.controller.ts index 8b9a77366d..72747f4a90 100644 --- a/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.controller.ts +++ b/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.controller.ts @@ -12,6 +12,7 @@ import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-a import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; +import { OrganizationMembershipService } from "@/lib/services/organization-membership.service"; import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input"; import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/update-organization-team-membership.input"; @@ -58,8 +59,9 @@ export class OrganizationsTeamsMembershipsController { constructor( private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService, - private readonly organizationsRepository: OrganizationsRepository - ) {} + private readonly organizationsRepository: OrganizationsRepository, + private readonly orgMembershipService: OrganizationMembershipService + ) { } @Get("/") @ApiOperation({ summary: "Get all memberships" }) @@ -168,6 +170,9 @@ export class OrganizationsTeamsMembershipsController { }; } + + // TODO: Refactor to use inviteMembersWithNoInviterPermissionCheck when it is moved to a Service + // See: packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts @Roles("TEAM_ADMIN") @PlatformPlan("ESSENTIALS") @Post("/") @@ -184,7 +189,21 @@ export class OrganizationsTeamsMembershipsController { throw new UnprocessableEntityException("User is not part of the Organization"); } - const membership = await this.organizationsTeamsMembershipsService.createOrgTeamMembership(teamId, data); + const shouldAutoAccept = await this.orgMembershipService.shouldAutoAccept({ + organizationId: orgId, + userEmail: user.email, + }); + + // ALWAYS override when email matches - prevents pending memberships + // Remember organizations expect added team member to automatically start receiving bookings for the team event + const acceptedStatus = shouldAutoAccept ? true : (data.accepted ?? false); + + const membershipData = { ...data, accepted: acceptedStatus }; + const membership = await this.organizationsTeamsMembershipsService.createOrgTeamMembership( + teamId, + membershipData + ); + if (membership.accepted) { try { await updateNewTeamMemberEventTypes(user.id, teamId); diff --git a/apps/api/v2/test/fixtures/repository/organization.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/organization.repository.fixture.ts index 83b6749799..1cac91ceb9 100644 --- a/apps/api/v2/test/fixtures/repository/organization.repository.fixture.ts +++ b/apps/api/v2/test/fixtures/repository/organization.repository.fixture.ts @@ -37,6 +37,20 @@ export class OrganizationRepositoryFixture { }); } + async updateSettings( + teamId: Team["id"], + settings: { + orgAutoAcceptEmail?: string; + isOrganizationVerified?: boolean; + isOrganizationConfigured?: boolean; + } + ) { + return this.prismaWriteClient.organizationSettings.update({ + where: { organizationId: teamId }, + data: settings, + }); + } + async delete(teamId: Team["id"]) { return await this.prismaWriteClient.$transaction(async (prisma) => { await prisma.organizationSettings.delete({ diff --git a/apps/api/v2/tsconfig.json b/apps/api/v2/tsconfig.json index 03473dcc05..de9060cfa2 100644 --- a/apps/api/v2/tsconfig.json +++ b/apps/api/v2/tsconfig.json @@ -29,7 +29,8 @@ "@calcom/platform-libraries/conferencing": ["../../../packages/platform/libraries/conferencing.ts"], "@calcom/platform-libraries/repositories": ["../../../packages/platform/libraries/repositories.ts"], "@calcom/platform-libraries/bookings": ["../../../packages/platform/libraries/bookings.ts"], - "@calcom/platform-libraries/private-links": ["../../../packages/platform/libraries/private-links.ts"] + "@calcom/platform-libraries/private-links": ["../../../packages/platform/libraries/private-links.ts"], + "@calcom/platform-libraries/organizations": ["../../../packages/platform/libraries/organizations.ts"] }, "incremental": true, "skipLibCheck": true, diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx index e9ae0e7de0..21a6665ca5 100644 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx +++ b/apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx @@ -6,7 +6,7 @@ import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable"; import { getScheduleListItemData } from "@calcom/lib/schedules/transformers/getScheduleListItemData"; @@ -61,8 +61,9 @@ const Page = async ({ searchParams: _searchParams }: PageProps) => { }; const organizationId = session?.user?.profile?.organizationId ?? session?.user.org?.id; + const organizationRepository = getOrganizationRepository(); const isOrgPrivate = organizationId - ? await OrganizationRepository.checkIfPrivate({ + ? await organizationRepository.checkIfPrivate({ orgId: organizationId, }) : false; diff --git a/apps/web/app/(use-page-wrapper)/settings/(admin-layout)/admin/organizations/[id]/edit/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(admin-layout)/admin/organizations/[id]/edit/page.tsx index 5e35f5db78..e4f8941d35 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(admin-layout)/admin/organizations/[id]/edit/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(admin-layout)/admin/organizations/[id]/edit/page.tsx @@ -4,12 +4,13 @@ import { z } from "zod"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; import { OrgForm } from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgEditPage"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; const orgIdSchema = z.object({ id: z.coerce.number() }); export const generateMetadata = async ({ params }: { params: Params }) => { + const organizationRepository = getOrganizationRepository(); const input = orgIdSchema.safeParse(await params); if (!input.success) { return await _generateMetadata( @@ -21,7 +22,7 @@ export const generateMetadata = async ({ params }: { params: Params }) => { ); } - const org = await OrganizationRepository.adminFindById({ id: input.data.id }); + const org = await organizationRepository.adminFindById({ id: input.data.id }); return await _generateMetadata( (t) => `${t("editing_org")}: ${org.name}`, @@ -33,11 +34,12 @@ export const generateMetadata = async ({ params }: { params: Params }) => { }; const Page = async ({ params }: { params: Params }) => { + const organizationRepository = getOrganizationRepository(); const input = orgIdSchema.safeParse(await params); if (!input.success) throw new Error("Invalid access"); - const org = await OrganizationRepository.adminFindById({ id: input.data.id }); + const org = await organizationRepository.adminFindById({ id: input.data.id }); const t = await getTranslate(); return ( diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/teams/other/(main-page)/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/teams/other/(main-page)/page.tsx index 028a767e7e..1359594aae 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/teams/other/(main-page)/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/teams/other/(main-page)/page.tsx @@ -2,7 +2,7 @@ import { _generateMetadata, getTranslate } from "app/_utils"; import { redirect } from "next/navigation"; import { OtherTeamsListing } from "@calcom/features/ee/organizations/pages/components/OtherTeamsListing"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; import { validateUserHasOrg } from "../../../actions/validateUserHasOrg"; @@ -24,11 +24,12 @@ const Page = async () => { redirect("/auth/login"); } const organizationId = session?.user?.org?.id; + const organizationRepository = getOrganizationRepository(); const otherTeams = organizationId - ? await OrganizationRepository.findTeamsInOrgIamNotPartOf({ - userId: session?.user.id, - parentId: organizationId, - }) + ? await organizationRepository.findTeamsInOrgIamNotPartOf({ + userId: session?.user.id, + parentId: organizationId, + }) : []; return ( diff --git a/apps/web/lib/pages/auth/verify-email.test.ts b/apps/web/lib/pages/auth/verify-email.test.ts index d11e370cdd..ad568abde0 100644 --- a/apps/web/lib/pages/auth/verify-email.test.ts +++ b/apps/web/lib/pages/auth/verify-email.test.ts @@ -10,7 +10,6 @@ import { moveUserToMatchingOrg } from "./verify-email"; // TODO: This test passes but coverage is very low. vi.mock("@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler"); -vi.mock("@calcom/features/ee/organizations/repositories/OrganizationRepository"); vi.mock("@calcom/prisma", () => { return { prisma: vi.fn(), @@ -37,7 +36,7 @@ describe("moveUserToMatchingOrg", () => { }); it("should not proceed if no matching organization is found", async () => { - organizationScenarios.OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.fakeNoMatch(); + organizationScenarios.organizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.fakeNoMatch(); await moveUserToMatchingOrg({ email }); @@ -64,7 +63,7 @@ describe("moveUserToMatchingOrg", () => { requestedSlug: "requested-test-org", }; - organizationScenarios.OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.fakeReturnOrganization( + organizationScenarios.organizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.fakeReturnOrganization( org, { email } ); @@ -85,7 +84,7 @@ describe("moveUserToMatchingOrg", () => { requestedSlug: "requested-test-org", }; - organizationScenarios.OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.fakeReturnOrganization( + organizationScenarios.organizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.fakeReturnOrganization( org, { email } ); diff --git a/apps/web/lib/pages/auth/verify-email.ts b/apps/web/lib/pages/auth/verify-email.ts index 720f1b09b9..0b11c692f0 100644 --- a/apps/web/lib/pages/auth/verify-email.ts +++ b/apps/web/lib/pages/auth/verify-email.ts @@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { z } from "zod"; import dayjs from "@calcom/dayjs"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service"; import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service"; import { WEBAPP_URL } from "@calcom/lib/constants"; @@ -21,7 +21,8 @@ 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.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email }); + const organizationRepository = getOrganizationRepository(); + const org = await organizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email }); if (!org) { return; diff --git a/apps/web/lib/video/[uid]/getServerSideProps.ts b/apps/web/lib/video/[uid]/getServerSideProps.ts index 5d28cc47cd..a278f3f4c4 100644 --- a/apps/web/lib/video/[uid]/getServerSideProps.ts +++ b/apps/web/lib/video/[uid]/getServerSideProps.ts @@ -8,7 +8,7 @@ import { } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository"; import { getCalVideoReference } from "@calcom/features/get-cal-video-reference"; import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; @@ -167,8 +167,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { ).profile : null; + const organizationRepository = getOrganizationRepository(); + const calVideoLogo = profile?.organization - ? await OrganizationRepository.findCalVideoLogoByOrgId({ id: profile.organization.id }) + ? await organizationRepository.findCalVideoLogoByOrgId({ id: profile.organization.id }) : null; //daily.co calls have a 14 days exit buffer when a user enters a call when it's not available it will trigger the modals diff --git a/packages/app-store/delegationCredential.test.ts b/packages/app-store/delegationCredential.test.ts index 37f09eece3..8f35b14757 100644 --- a/packages/app-store/delegationCredential.test.ts +++ b/packages/app-store/delegationCredential.test.ts @@ -6,7 +6,7 @@ import { metadata as googleCalendarMetadata } from "@calcom/app-store/googlecale import { metadata as googleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata"; import type { ServiceAccountKey } from "@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository"; import { DelegationCredentialRepository } from "@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { organizationRepositoryMock } from "@calcom/features/ee/organizations/__mocks__/organizationMock"; import { SMSLockState, RRTimestampBasis } from "@calcom/prisma/enums"; import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential"; @@ -20,11 +20,8 @@ import { getAllDelegationCredentialsForUserIncludeServiceAccountKey, } from "./delegationCredential"; -// Mock OrganizationRepository -vi.mock("@calcom/features/ee/organizations/repositories/OrganizationRepository", () => ({ - OrganizationRepository: { - findByMemberEmail: vi.fn(), - }, +vi.mock("@calcom/prisma", () => ({ + prisma: {}, })); // Mock DelegationCredentialRepository @@ -187,7 +184,7 @@ describe("getAllDelegationCredentialsForUserIncludeServiceAccountKey", () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(OrganizationRepository.findByMemberEmail).mockResolvedValue(mockOrganization); + organizationRepositoryMock.findByMemberEmail.mockResolvedValue(mockOrganization); }); it("should return empty array when no DelegationCredential found", async () => { diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index 59d5d88473..bab3b10274 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -16,7 +16,7 @@ import { CredentialRepository } from "@calcom/features/credentials/repositories/ import createUsersAndConnectToOrg from "@calcom/features/ee/dsync/lib/users/createUsersAndConnectToOrg"; import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider"; import { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository"; import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; @@ -389,7 +389,8 @@ if (isSAMLLoginEnabled) { const hostedCal = Boolean(HOSTED_CAL_FEATURES); if (hostedCal && email) { const domain = getDomainFromEmail(email); - const org = await OrganizationRepository.getVerifiedOrganizationByAutoAcceptEmailDomain(domain); + const organizationRepository = getOrganizationRepository(); + const org = await organizationRepository.getVerifiedOrganizationByAutoAcceptEmailDomain(domain); if (org) { const createUsersAndConnectToOrgProps = { emailsToCreate: [email], @@ -407,7 +408,7 @@ if (isSAMLLoginEnabled) { } if (!user) throw new Error(ErrorCode.UserNotFound); } - const [userProfile] = user?.allProfiles; + const [userProfile] = user?.allProfiles ?? []; return { id: id as unknown as number, firstName, diff --git a/packages/features/bookings/di/BookingCancelService.container.ts b/packages/features/bookings/di/BookingCancelService.container.ts index 6b7cabaa43..a238759467 100644 --- a/packages/features/bookings/di/BookingCancelService.container.ts +++ b/packages/features/bookings/di/BookingCancelService.container.ts @@ -1,9 +1,9 @@ -import { createContainer } from "@calcom/lib/di/di"; +import { createContainer } from "@calcom/features/di/di"; import { type BookingCancelService, moduleLoader as bookingCancelServiceModule, -} from "../modules/BookingCancelService.module"; +} from "./BookingCancelService.module"; const bookingCancelServiceContainer = createContainer(); diff --git a/packages/features/bookings/di/BookingCancelService.module.ts b/packages/features/bookings/di/BookingCancelService.module.ts index 8781e6e195..217fd40dd8 100644 --- a/packages/features/bookings/di/BookingCancelService.module.ts +++ b/packages/features/bookings/di/BookingCancelService.module.ts @@ -1,6 +1,6 @@ import { BookingCancelService } from "@calcom/features/bookings/lib/handleCancelBooking"; -import { bindModuleToClassOnToken, createModule } from "@calcom/lib/di/di"; -import { DI_TOKENS } from "@calcom/lib/di/tokens"; +import { bindModuleToClassOnToken, createModule } from "@calcom/features/di/di"; +import { DI_TOKENS } from "@calcom/features/di/tokens"; import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; const thisModule = createModule(); diff --git a/packages/features/delegation-credentials/repositories/DelegationCredentialRepository.test.ts b/packages/features/delegation-credentials/repositories/DelegationCredentialRepository.test.ts index a6e3ffc602..53a4f1bf97 100644 --- a/packages/features/delegation-credentials/repositories/DelegationCredentialRepository.test.ts +++ b/packages/features/delegation-credentials/repositories/DelegationCredentialRepository.test.ts @@ -2,22 +2,27 @@ import prismock from "../../../../tests/libs/__mocks__/prisma"; import { describe, expect, it, beforeEach, vi } from "vitest"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; import { encryptServiceAccountKey } from "@calcom/lib/server/serviceAccountKey"; import { DelegationCredentialRepository } from "./DelegationCredentialRepository"; -vi.mock("@calcom/features/ee/organizations/repositories/OrganizationRepository", () => ({ - OrganizationRepository: { - findByMemberEmail: vi.fn(), - }, +const mockOrganizationRepository = { + findByMemberEmail: vi.fn(), +}; + +vi.mock("@calcom/features/ee/organizations/di/OrganizationRepository.container", () => ({ + getOrganizationRepository: () => mockOrganizationRepository, +})); + +vi.mock("@calcom/prisma", () => ({ + prisma: {}, })); // Mock service account key functions vi.mock("@calcom/lib/crypto", async (importOriginal) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, symmetricEncrypt: vi.fn((serviceAccountKey) => { @@ -92,8 +97,8 @@ const createTestDelegationCredential = async (overrides = {}) => { }); }; -const setupOrganizationMock = (returnValue: any) => { - vi.mocked(OrganizationRepository.findByMemberEmail).mockResolvedValue(returnValue); +const setupOrganizationMock = (returnValue: { id: number } | null) => { + mockOrganizationRepository.findByMemberEmail.mockResolvedValue(returnValue); }; describe("DelegationCredentialRepository", () => { diff --git a/packages/features/delegation-credentials/repositories/DelegationCredentialRepository.ts b/packages/features/delegation-credentials/repositories/DelegationCredentialRepository.ts index 2cf8522b50..0049e863b8 100644 --- a/packages/features/delegation-credentials/repositories/DelegationCredentialRepository.ts +++ b/packages/features/delegation-credentials/repositories/DelegationCredentialRepository.ts @@ -1,4 +1,4 @@ -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import logger from "@calcom/lib/logger"; import { serviceAccountKeySchema, @@ -137,7 +137,8 @@ export class DelegationCredentialRepository { prefix: ["findUniqueByOrgMemberEmailIncludeSensitiveServiceAccountKey"], }); log.debug("called with", { email }); - const organization = await OrganizationRepository.findByMemberEmail({ email }); + const organizationRepository = getOrganizationRepository(); + const organization = await organizationRepository.findByMemberEmail({ email }); if (!organization) { log.debug("Email not found in any organization:", email); return null; diff --git a/packages/features/di/tokens.ts b/packages/features/di/tokens.ts index 1e56074e12..a6f600b3b5 100644 --- a/packages/features/di/tokens.ts +++ b/packages/features/di/tokens.ts @@ -1,6 +1,6 @@ import { BOOKING_DI_TOKENS } from "@calcom/features/bookings/di/tokens"; import { HASHED_LINK_DI_TOKENS } from "@calcom/features/hashedLink/di/tokens"; - +import { ORGANIZATION_DI_TOKENS } from "@calcom/features/ee/organizations/di/tokens"; import { WATCHLIST_DI_TOKENS } from "./watchlist/Watchlist.tokens"; export const DI_TOKENS = { @@ -63,4 +63,5 @@ export const DI_TOKENS = { ...HASHED_LINK_DI_TOKENS, // Watchlist service tokens ...WATCHLIST_DI_TOKENS, + ...ORGANIZATION_DI_TOKENS, }; diff --git a/packages/features/ee/impersonation/lib/ImpersonationProvider.test.ts b/packages/features/ee/impersonation/lib/ImpersonationProvider.test.ts index 28ea23d489..c9116b0d8a 100644 --- a/packages/features/ee/impersonation/lib/ImpersonationProvider.test.ts +++ b/packages/features/ee/impersonation/lib/ImpersonationProvider.test.ts @@ -25,7 +25,7 @@ const session: Session = { vi.mock("@calcom/prisma", () => { return { - default: vi.fn(), + prisma: vi.fn(), }; }); diff --git a/packages/features/ee/organizations/__mocks__/organizationMock.ts b/packages/features/ee/organizations/__mocks__/organizationMock.ts index 0c7e23d9ae..b22cfd7870 100644 --- a/packages/features/ee/organizations/__mocks__/organizationMock.ts +++ b/packages/features/ee/organizations/__mocks__/organizationMock.ts @@ -1,23 +1,24 @@ import { vi, beforeEach } from "vitest"; -import { mockReset, mockDeep } from "vitest-mock-extended"; +import { mockDeep, mockReset } from "vitest-mock-extended"; -import type * as organization from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import type { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; + +const mockedSingleton = mockDeep(); + +vi.mock("@calcom/features/ee/organizations/di/OrganizationRepository.container", () => ({ + getOrganizationRepository: () => mockedSingleton, +})); -vi.mock("@calcom/features/ee/organizations/repositories/OrganizationRepository", () => organizationMock); -type OrganizationModule = typeof organization; beforeEach(() => { - mockReset(organizationMock); + mockReset(mockedSingleton); }); -const organizationMock = mockDeep(); -const OrganizationRepository = organizationMock.OrganizationRepository; - export const organizationScenarios = { - OrganizationRepository: { + organizationRepository: { findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail: { // eslint-disable-next-line @typescript-eslint/no-explicit-any fakeReturnOrganization: (org: any, forInput: any) => { - OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.mockImplementation( + mockedSingleton.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.mockImplementation( (arg) => { if (forInput.email === arg.email) { return org; @@ -29,10 +30,11 @@ export const organizationScenarios = { ); }, fakeNoMatch: () => { - OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.mockResolvedValue(null); + mockedSingleton.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.mockResolvedValue(null); }, }, - } satisfies Partial>, -} satisfies Partial>; + }, +}; -export default organizationMock; +export { mockedSingleton as organizationRepositoryMock }; +export default { organizationRepository: mockedSingleton }; diff --git a/packages/features/ee/organizations/di/OrganizationMembershipService.module.ts b/packages/features/ee/organizations/di/OrganizationMembershipService.module.ts new file mode 100644 index 0000000000..22a2256e61 --- /dev/null +++ b/packages/features/ee/organizations/di/OrganizationMembershipService.module.ts @@ -0,0 +1,27 @@ +import { bindModuleToClassOnToken, createModule } from "@calcom/features/di/di"; +import { OrganizationMembershipService } from "@calcom/features/ee/organizations/lib/service/OrganizationMembershipService"; + +import { moduleLoader as organizationRepositoryModuleLoader } from "./OrganizationRepository.module"; +import { ORGANIZATION_DI_TOKENS } from "./tokens"; + +const thisModule = createModule(); +const token = ORGANIZATION_DI_TOKENS.ORGANIZATION_MEMBERSHIP_SERVICE; +const moduleToken = ORGANIZATION_DI_TOKENS.ORGANIZATION_MEMBERSHIP_SERVICE_MODULE; + +const loadModule = bindModuleToClassOnToken({ + module: thisModule, + moduleToken, + token, + classs: OrganizationMembershipService, + depsMap: { + organizationRepository: organizationRepositoryModuleLoader, + }, +}); + +export const moduleLoader = { + token, + loadModule, +}; + +export type { OrganizationMembershipService }; + diff --git a/packages/features/ee/organizations/di/OrganizationRepository.container.ts b/packages/features/ee/organizations/di/OrganizationRepository.container.ts new file mode 100644 index 0000000000..bf8fe15e2d --- /dev/null +++ b/packages/features/ee/organizations/di/OrganizationRepository.container.ts @@ -0,0 +1,14 @@ +import { createContainer } from "@calcom/features/di/di"; + +import { + type OrganizationRepository, + moduleLoader as organizationRepositoryModule, +} from "./OrganizationRepository.module"; + +const organizationRepositoryContainer = createContainer(); + +export function getOrganizationRepository(): OrganizationRepository { + organizationRepositoryModule.loadModule(organizationRepositoryContainer); + + return organizationRepositoryContainer.get(organizationRepositoryModule.token); +} diff --git a/packages/features/ee/organizations/di/OrganizationRepository.module.ts b/packages/features/ee/organizations/di/OrganizationRepository.module.ts new file mode 100644 index 0000000000..ba41eb1a7e --- /dev/null +++ b/packages/features/ee/organizations/di/OrganizationRepository.module.ts @@ -0,0 +1,26 @@ +import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di"; +import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; + +import { ORGANIZATION_DI_TOKENS } from "./tokens"; + +export const organizationRepositoryModule = createModule(); +const token = ORGANIZATION_DI_TOKENS.ORGANIZATION_REPOSITORY; +const moduleToken = ORGANIZATION_DI_TOKENS.ORGANIZATION_REPOSITORY_MODULE; +const loadModule = bindModuleToClassOnToken({ + module: organizationRepositoryModule, + moduleToken, + token, + classs: OrganizationRepository, + depsMap: { + prismaClient: prismaModuleLoader, + }, +}); + +export const moduleLoader: ModuleLoader = { + token, + loadModule, +}; + +export type { OrganizationRepository }; + diff --git a/packages/features/ee/organizations/di/tokens.ts b/packages/features/ee/organizations/di/tokens.ts new file mode 100644 index 0000000000..c6b23698ee --- /dev/null +++ b/packages/features/ee/organizations/di/tokens.ts @@ -0,0 +1,6 @@ +export const ORGANIZATION_DI_TOKENS = { + ORGANIZATION_REPOSITORY: Symbol("OrganizationRepository"), + ORGANIZATION_REPOSITORY_MODULE: Symbol("OrganizationRepositoryModule"), + ORGANIZATION_MEMBERSHIP_SERVICE: Symbol("OrganizationMembershipService"), + ORGANIZATION_MEMBERSHIP_SERVICE_MODULE: Symbol("OrganizationMembershipServiceModule"), +}; diff --git a/packages/features/ee/organizations/lib/OrganizationPermissionService.ts b/packages/features/ee/organizations/lib/OrganizationPermissionService.ts index 88f962e5a2..e21824e201 100644 --- a/packages/features/ee/organizations/lib/OrganizationPermissionService.ts +++ b/packages/features/ee/organizations/lib/OrganizationPermissionService.ts @@ -1,4 +1,4 @@ -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import { UserPermissionRole } from "@calcom/kysely/types"; import { ORGANIZATION_SELF_SERVE_MIN_SEATS, ORGANIZATION_SELF_SERVE_PRICE } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; @@ -39,7 +39,8 @@ export class OrganizationPermissionService { * If an onboarding is complete then it also means that org is created already. */ async hasConflictingOrganization({ slug }: { slug: string }): Promise { - return !!(await OrganizationRepository.findBySlug({ slug })); + const organizationRepository = getOrganizationRepository(); + return !!(await organizationRepository.findBySlug({ slug })); } async hasCompletedOnboarding(email: string): Promise { diff --git a/packages/features/ee/organizations/lib/ensureOrganizationIsReviewed.ts b/packages/features/ee/organizations/lib/ensureOrganizationIsReviewed.ts index 14f34cc556..f996a1829f 100644 --- a/packages/features/ee/organizations/lib/ensureOrganizationIsReviewed.ts +++ b/packages/features/ee/organizations/lib/ensureOrganizationIsReviewed.ts @@ -1,12 +1,13 @@ -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; /** * It assumes that a user can only impersonate the members of the organization he is logged in to. * Note: Ensuring that one organization's member can't impersonate other organization's member isn't the job of this function */ export async function ensureOrganizationIsReviewed(loggedInUserOrgId: number | undefined) { + const organizationRepository = getOrganizationRepository(); if (loggedInUserOrgId) { - const org = await OrganizationRepository.findByIdIncludeOrganizationSettings({ + const org = await organizationRepository.findByIdIncludeOrganizationSettings({ id: loggedInUserOrgId, }); diff --git a/packages/features/ee/organizations/lib/repository/IOrganizationRepository.d.ts b/packages/features/ee/organizations/lib/repository/IOrganizationRepository.d.ts new file mode 100644 index 0000000000..fca9b9ca0d --- /dev/null +++ b/packages/features/ee/organizations/lib/repository/IOrganizationRepository.d.ts @@ -0,0 +1,6 @@ +export interface IOrganizationRepository { + getOrganizationAutoAcceptSettings(organizationId: number): Promise<{ + orgAutoAcceptEmail: string | null; + isOrganizationVerified: boolean | null; + } | null>; +} \ No newline at end of file diff --git a/packages/features/ee/organizations/lib/service/OrganizationMembershipService.ts b/packages/features/ee/organizations/lib/service/OrganizationMembershipService.ts new file mode 100644 index 0000000000..2a25883655 --- /dev/null +++ b/packages/features/ee/organizations/lib/service/OrganizationMembershipService.ts @@ -0,0 +1,37 @@ +import type { IOrganizationRepository } from "../repository/IOrganizationRepository"; +export interface IOrganizationMembershipServiceDependencies { + organizationRepository: IOrganizationRepository; +} + +export class OrganizationMembershipService { + constructor(private readonly deps: IOrganizationMembershipServiceDependencies) { } + + /** + * Determines if user should be auto-accepted to an organization or its sub-teams based on email domain + */ + async shouldAutoAccept({ + organizationId, + userEmail, + }: { + organizationId: number; + userEmail: string; + }): Promise { + const orgSettings = await this.deps.organizationRepository.getOrganizationAutoAcceptSettings( + organizationId + ); + + if (!orgSettings) return false; + + const { orgAutoAcceptEmail, isOrganizationVerified } = orgSettings; + + if (!isOrganizationVerified || !orgAutoAcceptEmail) return false; + + // Case-insensitive comparison (email domains are case-insensitive per RFC) + const emailDomain = userEmail.split("@")[1]?.trim().toLowerCase(); + const autoAcceptEmailDomain = orgAutoAcceptEmail.trim().toLowerCase(); + + if (!emailDomain) return false; + + return emailDomain === autoAcceptEmailDomain; + } +} diff --git a/packages/features/ee/organizations/lib/service/onboarding/BaseOnboardingService.ts b/packages/features/ee/organizations/lib/service/onboarding/BaseOnboardingService.ts index 363241bf27..ea90348212 100644 --- a/packages/features/ee/organizations/lib/service/onboarding/BaseOnboardingService.ts +++ b/packages/features/ee/organizations/lib/service/onboarding/BaseOnboardingService.ts @@ -8,7 +8,7 @@ import { findUserToBeOrgOwner, setupDomain, } from "@calcom/features/ee/organizations/lib/server/orgCreationUtils"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; import { WEBAPP_URL } from "@calcom/lib/constants"; @@ -296,8 +296,9 @@ export abstract class BaseOnboardingService implements IOrganizationOnboardingSe owner: NonNullable>>; orgData: OrganizationData; }) { + const organizationRepository = getOrganizationRepository(); const orgOwnerTranslation = await getTranslation(owner.locale || "en", "common"); - let organization = orgData.id ? await OrganizationRepository.findById({ id: orgData.id }) : null; + let organization = orgData.id ? await organizationRepository.findById({ id: orgData.id }) : null; if (organization) { log.info( @@ -328,7 +329,7 @@ export abstract class BaseOnboardingService implements IOrganizationOnboardingSe const nonOrgUsername = owner.username || ""; // Create organization first to get the ID - const orgCreationResult = await OrganizationRepository.createWithExistingUserAsOwner({ + const orgCreationResult = await organizationRepository.createWithExistingUserAsOwner({ orgData: { ...orgData, // Don't pass brand assets yet - will be uploaded after org is created @@ -394,9 +395,10 @@ export abstract class BaseOnboardingService implements IOrganizationOnboardingSe email: string; orgData: OrganizationData; }) { + const organizationRepository = getOrganizationRepository(); let organization = orgData.id - ? await OrganizationRepository.findById({ id: orgData.id }) - : await OrganizationRepository.findBySlug({ slug: orgData.slug }); + ? await organizationRepository.findById({ id: orgData.id }) + : await organizationRepository.findBySlug({ slug: orgData.slug }); if (organization) { log.info( @@ -410,7 +412,7 @@ export abstract class BaseOnboardingService implements IOrganizationOnboardingSe return { organization, owner }; } - const orgCreationResult = await OrganizationRepository.createWithNonExistentOwner({ + const orgCreationResult = await organizationRepository.createWithNonExistentOwner({ orgData: { ...orgData, // To be uploaded after org is created @@ -670,7 +672,8 @@ export abstract class BaseOnboardingService implements IOrganizationOnboardingSe } const existingMetadata = teamMetadataStrictSchema.parse(organization.metadata); - const updatedOrganization = await OrganizationRepository.updateStripeSubscriptionDetails({ + const organizationRepository = getOrganizationRepository(); + const updatedOrganization = await organizationRepository.updateStripeSubscriptionDetails({ id: organization.id, stripeSubscriptionId: paymentSubscriptionId, stripeSubscriptionItemId: paymentSubscriptionItemId, @@ -680,7 +683,8 @@ export abstract class BaseOnboardingService implements IOrganizationOnboardingSe } protected async hasConflictingOrganization({ slug, onboardingId }: { slug: string; onboardingId: string }) { - const organization = await OrganizationRepository.findBySlugIncludeOnboarding({ slug }); + const organizationRepository = getOrganizationRepository(); + const organization = await organizationRepository.findBySlugIncludeOnboarding({ slug }); if (!organization?.organizationOnboarding) { return false; } diff --git a/packages/features/ee/organizations/lib/service/onboarding/BillingEnabledOrgOnboardingService.ts b/packages/features/ee/organizations/lib/service/onboarding/BillingEnabledOrgOnboardingService.ts index daec8c3b1d..f8f059f4c0 100644 --- a/packages/features/ee/organizations/lib/service/onboarding/BillingEnabledOrgOnboardingService.ts +++ b/packages/features/ee/organizations/lib/service/onboarding/BillingEnabledOrgOnboardingService.ts @@ -1,5 +1,5 @@ import { findUserToBeOrgOwner } from "@calcom/features/ee/organizations/lib/server/orgCreationUtils"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import { IS_SELF_HOSTED } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; @@ -143,6 +143,7 @@ export class BillingEnabledOrgOnboardingService extends BaseOnboardingService { organizationOnboarding: OrganizationOnboardingData, paymentDetails?: { subscriptionId: string; subscriptionItemId: string } ): Promise<{ organization: Team; owner: User }> { + const organizationRepository = getOrganizationRepository(); log.info( "createOrganization (billing-enabled)", safeStringify({ @@ -245,7 +246,7 @@ export class BillingEnabledOrgOnboardingService extends BaseOnboardingService { if (!organization.slug) { try { - const { slug } = await OrganizationRepository.setSlug({ + const { slug } = await organizationRepository.setSlug({ id: organization.id, slug: organizationOnboarding.slug, }); diff --git a/packages/features/ee/organizations/lib/service/onboarding/SelfHostedOnboardingService.ts b/packages/features/ee/organizations/lib/service/onboarding/SelfHostedOnboardingService.ts index a021439076..4a85d589ca 100644 --- a/packages/features/ee/organizations/lib/service/onboarding/SelfHostedOnboardingService.ts +++ b/packages/features/ee/organizations/lib/service/onboarding/SelfHostedOnboardingService.ts @@ -1,6 +1,6 @@ import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService"; import { findUserToBeOrgOwner } from "@calcom/features/ee/organizations/lib/server/orgCreationUtils"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import { IS_SELF_HOSTED } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; @@ -113,6 +113,7 @@ export class SelfHostedOrganizationOnboardingService extends BaseOnboardingServi async createOrganization( organizationOnboarding: OrganizationOnboardingData ): Promise<{ organization: Team; owner: User }> { + const organizationRepository = getOrganizationRepository(); log.info( "createOrganization (self-hosted)", safeStringify({ @@ -205,7 +206,7 @@ export class SelfHostedOrganizationOnboardingService extends BaseOnboardingServi if (!organization.slug) { try { - const { slug } = await OrganizationRepository.setSlug({ + const { slug } = await organizationRepository.setSlug({ id: organization.id, slug: organizationOnboarding.slug, }); diff --git a/packages/features/ee/organizations/pages/components/OtherTeamList.tsx b/packages/features/ee/organizations/pages/components/OtherTeamList.tsx index 1deadc4b51..c5d4691d49 100644 --- a/packages/features/ee/organizations/pages/components/OtherTeamList.tsx +++ b/packages/features/ee/organizations/pages/components/OtherTeamList.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; -import type { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import type { OrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.module"; import { trackFormbricksAction } from "@calcom/features/formbricks/formbricks-client"; import { trpc } from "@calcom/trpc/react"; import { showToast } from "@calcom/ui/components/toast"; @@ -10,7 +10,7 @@ import { showToast } from "@calcom/ui/components/toast"; import OtherTeamListItem from "./OtherTeamListItem"; interface Props { - teams: Awaited>; + teams: Awaited>; pending?: boolean; } diff --git a/packages/features/ee/organizations/pages/components/OtherTeamsListing.tsx b/packages/features/ee/organizations/pages/components/OtherTeamsListing.tsx index 50c39cb32c..23a3869660 100644 --- a/packages/features/ee/organizations/pages/components/OtherTeamsListing.tsx +++ b/packages/features/ee/organizations/pages/components/OtherTeamsListing.tsx @@ -1,13 +1,13 @@ "use client"; -import type { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import type { OrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.module"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { EmptyScreen } from "@calcom/ui/components/empty-screen"; import OtherTeamList from "./OtherTeamList"; type OtherTeamsListingProps = { - teams: Awaited>; + teams: Awaited>; }; export function OtherTeamsListing({ teams }: OtherTeamsListingProps) { const { t } = useLocale(); diff --git a/packages/features/ee/organizations/repositories/OrganizationRepository.test.ts b/packages/features/ee/organizations/repositories/OrganizationRepository.test.ts index 4310b40abe..1df9ec9a42 100644 --- a/packages/features/ee/organizations/repositories/OrganizationRepository.test.ts +++ b/packages/features/ee/organizations/repositories/OrganizationRepository.test.ts @@ -6,9 +6,11 @@ import { OrganizationRepository } from "@calcom/features/ee/organizations/reposi import type { Prisma } from "@calcom/prisma/client"; vi.mock("@calcom/lib/server/repository/teamUtils", () => ({ - getParsedTeam: (org: any) => org, + getParsedTeam: (org: T) => org, })); +const organizationRepository = new OrganizationRepository({ prismaClient: prismock }); + async function createOrganization( data: Prisma.TeamCreateInput & { organizationSettings: { @@ -72,7 +74,7 @@ beforeEach(async () => { describe("Organization.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail", () => { it("should return null if no organization matches the email domain", async () => { - const result = await OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ + const result = await organizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email: "test@example.com", }); @@ -84,7 +86,7 @@ describe("Organization.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail", () = await createReviewedOrganization({ name: "Test Org 2", orgAutoAcceptEmail: "example.com" }); await expect( - OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email: "test@example.com" }) + organizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email: "test@example.com" }) ).rejects.toThrow("Multiple organizations found with the same auto accept email domain"); }); @@ -94,7 +96,7 @@ describe("Organization.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail", () = orgAutoAcceptEmail: "example.com", }); - const result = await OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ + const result = await organizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email: "test@example.com", }); @@ -104,7 +106,7 @@ describe("Organization.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail", () = it("should not confuse a team with organization", async () => { await createTeam({ name: "Test Team", orgAutoAcceptEmail: "example.com" }); - const result = await OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ + const result = await organizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email: "test@example.com", }); @@ -114,7 +116,7 @@ describe("Organization.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail", () = it("should correctly match orgAutoAcceptEmail", async () => { await createReviewedOrganization({ name: "Test Org", orgAutoAcceptEmail: "noexample.com" }); - const result = await OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ + const result = await organizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email: "test@example.com", }); @@ -129,7 +131,7 @@ describe("Organization.getVerifiedOrganizationByAutoAcceptEmailDomain", () => { organizationSettings: { create: { orgAutoAcceptEmail: "cal.com", isOrganizationVerified: true } }, }); - const result = await OrganizationRepository.getVerifiedOrganizationByAutoAcceptEmailDomain("cal.com"); + const result = await organizationRepository.getVerifiedOrganizationByAutoAcceptEmailDomain("cal.com"); expect(result).toEqual({ id: verifiedOrganization.id, @@ -145,7 +147,7 @@ describe("Organization.getVerifiedOrganizationByAutoAcceptEmailDomain", () => { organizationSettings: { create: { orgAutoAcceptEmail: "cal.com", isOrganizationVerified: false } }, }); - const result = await OrganizationRepository.getVerifiedOrganizationByAutoAcceptEmailDomain("cal.com"); + const result = await organizationRepository.getVerifiedOrganizationByAutoAcceptEmailDomain("cal.com"); expect(result).toEqual(null); }); @@ -169,7 +171,7 @@ describe("Organization.create", () => { bannerUrl: "https://example.com/banner.jpg", }; - const organization = await OrganizationRepository.create(orgData); + const organization = await organizationRepository.create(orgData); expect(organization).toMatchObject({ name: "Test Organization", @@ -198,7 +200,7 @@ describe("Organization.create", () => { bannerUrl: null, }; - const organization = await OrganizationRepository.create(orgData); + const organization = await organizationRepository.create(orgData); expect(organization).toMatchObject({ name: "Test Organization", diff --git a/packages/features/ee/organizations/repositories/OrganizationRepository.ts b/packages/features/ee/organizations/repositories/OrganizationRepository.ts index ebe32f414a..7ab25073d0 100644 --- a/packages/features/ee/organizations/repositories/OrganizationRepository.ts +++ b/packages/features/ee/organizations/repositories/OrganizationRepository.ts @@ -6,7 +6,7 @@ import { UserRepository } from "@calcom/features/users/repositories/UserReposito import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getParsedTeam } from "@calcom/lib/server/repository/teamUtils"; -import { prisma } from "@calcom/prisma"; +import type { PrismaClient } from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; import type { CreationSource } from "@calcom/prisma/enums"; import type { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils"; @@ -20,7 +20,13 @@ const orgSelect = { }; export class OrganizationRepository { - static async createWithExistingUserAsOwner({ + protected readonly prismaClient: PrismaClient; + + constructor(deps: { prismaClient: PrismaClient }) { + this.prismaClient = deps.prismaClient; + } + + async createWithExistingUserAsOwner({ orgData, owner, }: { @@ -57,7 +63,7 @@ export class OrganizationRepository { organizationId: organization.id, }); - await prisma.membership.create({ + await this.prismaClient.membership.create({ data: { createdAt: new Date(), userId: owner.id, @@ -69,7 +75,7 @@ export class OrganizationRepository { return { organization, ownerProfile }; } - static async createWithNonExistentOwner({ + async createWithNonExistentOwner({ orgData, owner, creationSource, @@ -97,7 +103,7 @@ export class OrganizationRepository { logger.debug("createWithNonExistentOwner", safeStringify({ orgData, owner })); const organization = await this.create(orgData); const ownerUsernameInOrg = getOrgUsernameFromEmail(owner.email, orgData.autoAcceptEmail); - const userRepo = new UserRepository(prisma); + const userRepo = new UserRepository(this.prismaClient); const ownerInDb = await userRepo.create({ email: owner.email, username: ownerUsernameInOrg, @@ -106,7 +112,7 @@ export class OrganizationRepository { creationSource, }); - await prisma.membership.create({ + await this.prismaClient.membership.create({ data: { createdAt: new Date(), userId: ownerInDb.id, @@ -125,7 +131,7 @@ export class OrganizationRepository { }; } - static async create(orgData: { + async create(orgData: { name: string; slug: string | null; isOrganizationConfigured: boolean; @@ -141,7 +147,7 @@ export class OrganizationRepository { bannerUrl: string | null; requestedSlug?: string | null; }) { - return await prisma.team.create({ + return await this.prismaClient.team.create({ data: { name: orgData.name, isOrganization: true, @@ -176,8 +182,8 @@ export class OrganizationRepository { }); } - static async findById({ id }: { id: number }) { - return prisma.team.findUnique({ + async findById({ id }: { id: number }) { + return this.prismaClient.team.findUnique({ where: { id, isOrganization: true, @@ -185,9 +191,9 @@ export class OrganizationRepository { }); } - static async findBySlug({ slug }: { slug: string }) { + async findBySlug({ slug }: { slug: string }) { // Slug is unique but could be null as well, so we can't use findUnique - return prisma.team.findFirst({ + return this.prismaClient.team.findFirst({ where: { slug, isOrganization: true, @@ -195,8 +201,8 @@ export class OrganizationRepository { }); } - static async findBySlugIncludeOnboarding({ slug }: { slug: string }) { - return prisma.team.findFirst({ + async findBySlugIncludeOnboarding({ slug }: { slug: string }) { + return this.prismaClient.team.findFirst({ where: { slug, isOrganization: true }, include: { organizationOnboarding: { @@ -213,8 +219,8 @@ export class OrganizationRepository { }); } - static async findByIdIncludeOrganizationSettings({ id }: { id: number }) { - return prisma.team.findUnique({ + async findByIdIncludeOrganizationSettings({ id }: { id: number }) { + return this.prismaClient.team.findUnique({ where: { id, isOrganization: true, @@ -226,9 +232,9 @@ export class OrganizationRepository { }); } - static async findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email }: { email: string }) { + async findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email }: { email: string }) { const emailDomain = email.split("@").at(-1); - const orgs = await prisma.team.findMany({ + const orgs = await this.prismaClient.team.findMany({ where: { isOrganization: true, isPlatform: false, @@ -254,8 +260,8 @@ export class OrganizationRepository { return getParsedTeam(org); } - static async findCurrentOrg({ userId, orgId }: { userId: number; orgId: number }) { - const membership = await prisma.membership.findUnique({ + async findCurrentOrg({ userId, orgId }: { userId: number; orgId: number }) { + const membership = await this.prismaClient.membership.findUnique({ where: { userId_teamId: { userId, @@ -267,7 +273,7 @@ export class OrganizationRepository { }, }); - const organizationSettings = await prisma.organizationSettings.findUnique({ + const organizationSettings = await this.prismaClient.organizationSettings.findUnique({ where: { organizationId: orgId, }, @@ -307,8 +313,8 @@ export class OrganizationRepository { }; } - static async findTeamsInOrgIamNotPartOf({ userId, parentId }: { userId: number; parentId: number | null }) { - const teamsInOrgIamNotPartOf = await prisma.team.findMany({ + async findTeamsInOrgIamNotPartOf({ userId, parentId }: { userId: number; parentId: number | null }) { + const teamsInOrgIamNotPartOf = await this.prismaClient.team.findMany({ where: { parentId, members: { @@ -329,8 +335,8 @@ export class OrganizationRepository { return teamsInOrgIamNotPartOf; } - static async adminFindById({ id }: { id: number }) { - const org = await prisma.team.findUnique({ + async adminFindById({ id }: { id: number }) { + const org = await this.prismaClient.team.findUnique({ where: { id, }, @@ -374,8 +380,8 @@ export class OrganizationRepository { return { ...org, metadata: parsedMetadata }; } - static async findByMemberEmail({ email }: { email: string }) { - const organization = await prisma.team.findFirst({ + async findByMemberEmail({ email }: { email: string }) { + const organization = await this.prismaClient.team.findFirst({ where: { isOrganization: true, members: { @@ -388,10 +394,10 @@ export class OrganizationRepository { return organization ?? null; } - static async findByMemberEmailId({ email }: { email: string }) { + async findByMemberEmailId({ email }: { email: string }) { const log = logger.getSubLogger({ prefix: ["findByMemberEmailId"] }); log.debug("called with", { email }); - const organization = await prisma.team.findFirst({ + const organization = await this.prismaClient.team.findFirst({ where: { isOrganization: true, members: { @@ -407,8 +413,8 @@ export class OrganizationRepository { return organization; } - static async findCalVideoLogoByOrgId({ id }: { id: number }) { - const org = await prisma.team.findUnique({ + async findCalVideoLogoByOrgId({ id }: { id: number }) { + const org = await this.prismaClient.team.findUnique({ where: { id, }, @@ -420,8 +426,8 @@ export class OrganizationRepository { return org?.calVideoLogo; } - static async getVerifiedOrganizationByAutoAcceptEmailDomain(domain: string) { - return await prisma.team.findFirst({ + async getVerifiedOrganizationByAutoAcceptEmailDomain(domain: string) { + return await this.prismaClient.team.findFirst({ where: { organizationSettings: { isOrganizationVerified: true, @@ -439,14 +445,14 @@ export class OrganizationRepository { }); } - static async setSlug({ id, slug }: { id: number; slug: string }) { - return await prisma.team.update({ + async setSlug({ id, slug }: { id: number; slug: string }) { + return await this.prismaClient.team.update({ where: { id, isOrganization: true }, data: { slug }, }); } - static async updateStripeSubscriptionDetails({ + async updateStripeSubscriptionDetails({ id, stripeSubscriptionId, stripeSubscriptionItemId, @@ -457,7 +463,7 @@ export class OrganizationRepository { stripeSubscriptionItemId: string; existingMetadata: z.infer; }) { - return await prisma.team.update({ + return await this.prismaClient.team.update({ where: { id, isOrganization: true }, data: { metadata: { @@ -469,8 +475,8 @@ export class OrganizationRepository { }); } - static async checkIfPrivate({ orgId }: { orgId: number }) { - const team = await prisma.team.findUnique({ + async checkIfPrivate({ orgId }: { orgId: number }) { + const team = await this.prismaClient.team.findUnique({ where: { id: orgId, isOrganization: true, @@ -482,4 +488,20 @@ export class OrganizationRepository { return team?.isPrivate ?? false; } + + async getOrganizationAutoAcceptSettings(organizationId: number) { + const org = await this.prismaClient.team.findUnique({ + where: { id: organizationId, isOrganization: true }, + select: { + organizationSettings: { + select: { + orgAutoAcceptEmail: true, + isOrganizationVerified: true, + }, + }, + }, + }); + + return org?.organizationSettings ?? null; + } } diff --git a/packages/features/ee/sso/lib/sso.ts b/packages/features/ee/sso/lib/sso.ts index f8007c18ea..0a80acc3f4 100644 --- a/packages/features/ee/sso/lib/sso.ts +++ b/packages/features/ee/sso/lib/sso.ts @@ -1,5 +1,5 @@ import createUsersAndConnectToOrg from "@calcom/features/ee/dsync/lib/users/createUsersAndConnectToOrg"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants"; import type { PrismaClient } from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; @@ -36,7 +36,8 @@ export const ssoTenantProduct = async (prisma: PrismaClient, email: string) => { }); const domain = email.split("@")[1]; - const organization = await OrganizationRepository.getVerifiedOrganizationByAutoAcceptEmailDomain(domain); + const organizationRepository = getOrganizationRepository(); + const organization = await organizationRepository.getVerifiedOrganizationByAutoAcceptEmailDomain(domain); if (!organization) throw new TRPCError({ diff --git a/packages/platform/libraries/organizations.ts b/packages/platform/libraries/organizations.ts new file mode 100644 index 0000000000..62ed49528c --- /dev/null +++ b/packages/platform/libraries/organizations.ts @@ -0,0 +1,4 @@ +export { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; +export { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +export { OrganizationMembershipService } from "@calcom/features/ee/organizations/lib/service/OrganizationMembershipService"; +export type { IOrganizationRepository } from "@calcom/features/ee/organizations/lib/repository/IOrganizationRepository"; diff --git a/packages/platform/libraries/package.json b/packages/platform/libraries/package.json index 4eb67fb3cf..cd6fbe04ec 100644 --- a/packages/platform/libraries/package.json +++ b/packages/platform/libraries/package.json @@ -79,6 +79,11 @@ "require": "./dist/bookings.cjs", "types": "./dist/bookings.d.ts" }, + "./organizations": { + "import": "./dist/organizations.js", + "require": "./dist/organizations.cjs", + "types": "./dist/organizations.d.ts" + }, "./private-links": { "import": "./dist/private-links.js", "require": "./dist/private-links.cjs", @@ -119,6 +124,9 @@ "bookings": [ "dist/bookings.d.ts" ], + "organizations": [ + "dist/organizations.d.ts" + ], "private-links": [ "dist/private-links.d.ts" ], diff --git a/packages/platform/libraries/vite.config.js b/packages/platform/libraries/vite.config.js index d975221306..a422d26c48 100644 --- a/packages/platform/libraries/vite.config.js +++ b/packages/platform/libraries/vite.config.js @@ -36,6 +36,7 @@ export default defineConfig({ conferencing: resolve(__dirname, "./conferencing.ts"), repositories: resolve(__dirname, "./repositories.ts"), bookings: resolve(__dirname, "./bookings.ts"), + organizations: resolve(__dirname, "./organizations.ts"), "private-links": resolve(__dirname, "./private-links.ts"), pbac: resolve(__dirname, "./pbac.ts"), }, diff --git a/packages/trpc/server/routers/viewer/organizations/adminGet.handler.ts b/packages/trpc/server/routers/viewer/organizations/adminGet.handler.ts index 9fb740b782..f30ee936be 100644 --- a/packages/trpc/server/routers/viewer/organizations/adminGet.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/adminGet.handler.ts @@ -1,4 +1,4 @@ -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import type { TrpcSessionUser } from "../../../types"; import type { TAdminGet } from "./adminGet.schema"; @@ -11,7 +11,8 @@ type AdminGetOptions = { }; export const adminGetHandler = async ({ input }: AdminGetOptions) => { - return await OrganizationRepository.adminFindById({ id: input.id }); + const organizationRepository = getOrganizationRepository(); + return await organizationRepository.adminFindById({ id: input.id }); }; export default adminGetHandler; diff --git a/packages/trpc/server/routers/viewer/organizations/create.handler.ts b/packages/trpc/server/routers/viewer/organizations/create.handler.ts index f9e3b38edb..062c18181a 100644 --- a/packages/trpc/server/routers/viewer/organizations/create.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/create.handler.ts @@ -4,7 +4,7 @@ import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains"; import { isNotACompanyEmail } from "@calcom/ee/organizations/lib/server/orgCreationUtils"; import { sendAdminOrganizationNotification, sendOrganizationCreationEmail } from "@calcom/emails"; import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; import { @@ -44,6 +44,7 @@ const getIPAddress = async (url: string): Promise => { * TODO: To be removed. We need to reuse the logic from orgCreationUtils like in intentToCreateOrgHandler */ export const createHandler = async ({ input, ctx }: CreateOptions) => { + const organizationRepository = getOrganizationRepository(); const { slug, name, @@ -189,7 +190,7 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => { // Create a new user and invite them as the owner of the organization if (!orgOwner) { - const data = await OrganizationRepository.createWithNonExistentOwner({ + const data = await organizationRepository.createWithNonExistentOwner({ orgData, owner: { email: orgOwnerEmail, @@ -246,7 +247,7 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => { } const nonOrgUsernameForOwner = orgOwner.username || ""; - const { organization, ownerProfile } = await OrganizationRepository.createWithExistingUserAsOwner({ + const { organization, ownerProfile } = await organizationRepository.createWithExistingUserAsOwner({ orgData, owner: { id: orgOwner.id, diff --git a/packages/trpc/server/routers/viewer/organizations/list.handler.ts b/packages/trpc/server/routers/viewer/organizations/list.handler.ts index 216c2a47b7..56ba349ec8 100644 --- a/packages/trpc/server/routers/viewer/organizations/list.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/list.handler.ts @@ -1,4 +1,4 @@ -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import { FeaturesRepository } from "@calcom/features/flags/features.repository"; import prisma from "@calcom/prisma"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; @@ -18,7 +18,8 @@ export const listHandler = async ({ ctx }: ListHandlerInput) => { throw new TRPCError({ code: "BAD_REQUEST", message: "You do not belong to an organization" }); } - const currentOrg = await OrganizationRepository.findCurrentOrg({ + const organizationRepository = getOrganizationRepository(); + const currentOrg = await organizationRepository.findCurrentOrg({ userId: ctx.user.id, orgId: organizationId, }); diff --git a/packages/trpc/server/routers/viewer/organizations/listOtherTeams.handler.ts b/packages/trpc/server/routers/viewer/organizations/listOtherTeams.handler.ts index e45d51a308..5cb823b349 100644 --- a/packages/trpc/server/routers/viewer/organizations/listOtherTeams.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/listOtherTeams.handler.ts @@ -1,4 +1,4 @@ -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import type { TrpcSessionUser } from "../../../types"; @@ -13,7 +13,8 @@ export const listOtherTeamHandler = async ({ ctx: { user } }: ListOptions) => { return []; } - return await OrganizationRepository.findTeamsInOrgIamNotPartOf({ + const organizationRepository = getOrganizationRepository(); + return await organizationRepository.findTeamsInOrgIamNotPartOf({ userId: user.id, parentId: user?.organization?.id ?? null, });