From ae1b844bea8d5a57686e9c42d62f33b4fb18d217 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Mon, 9 Dec 2024 19:49:12 +0530 Subject: [PATCH] chore: Remove pages/settings (#18064) * Remove unused org mkigration pages and delete pages/settings * Remove more unused code causing ts errors * Remove SettingsLayout --- apps/web/lib/orgMigration.test.ts | 1684 ----------------- .../pages/api/orgMigration/moveTeamToOrg.ts | 76 - .../pages/api/orgMigration/moveUserToOrg.ts | 75 - .../api/orgMigration/removeTeamFromOrg.ts | 63 - .../api/orgMigration/removeUserFromOrg.ts | 59 - .../orgMigrations/_OrgMigrationLayout.tsx | 33 - .../admin/orgMigrations/moveTeamToOrg.tsx | 153 -- .../admin/orgMigrations/moveUserToOrg.tsx | 188 -- .../admin/orgMigrations/removeTeamFromOrg.tsx | 117 -- .../admin/orgMigrations/removeUserFromOrg.tsx | 115 -- apps/web/playwright/change-username.e2e.ts | 3 +- apps/web/{ => playwright}/lib/orgMigration.ts | 181 -- .../organization-invitation.e2e.ts | 3 +- .../settings/layouts/SettingsLayout.tsx | 797 -------- 14 files changed, 2 insertions(+), 3545 deletions(-) delete mode 100644 apps/web/lib/orgMigration.test.ts delete mode 100644 apps/web/pages/api/orgMigration/moveTeamToOrg.ts delete mode 100644 apps/web/pages/api/orgMigration/moveUserToOrg.ts delete mode 100644 apps/web/pages/api/orgMigration/removeTeamFromOrg.ts delete mode 100644 apps/web/pages/api/orgMigration/removeUserFromOrg.ts delete mode 100644 apps/web/pages/settings/admin/orgMigrations/_OrgMigrationLayout.tsx delete mode 100644 apps/web/pages/settings/admin/orgMigrations/moveTeamToOrg.tsx delete mode 100644 apps/web/pages/settings/admin/orgMigrations/moveUserToOrg.tsx delete mode 100644 apps/web/pages/settings/admin/orgMigrations/removeTeamFromOrg.tsx delete mode 100644 apps/web/pages/settings/admin/orgMigrations/removeUserFromOrg.tsx rename apps/web/{ => playwright}/lib/orgMigration.ts (81%) delete mode 100644 packages/features/settings/layouts/SettingsLayout.tsx diff --git a/apps/web/lib/orgMigration.test.ts b/apps/web/lib/orgMigration.test.ts deleted file mode 100644 index ad3d71cfaa..0000000000 --- a/apps/web/lib/orgMigration.test.ts +++ /dev/null @@ -1,1684 +0,0 @@ -import prismock from "../../../tests/libs/__mocks__/prisma"; - -import { describe, expect, it } from "vitest"; -import type { z } from "zod"; - -import { WEBSITE_URL } from "@calcom/lib/constants"; -import logger from "@calcom/lib/logger"; -import { safeStringify } from "@calcom/lib/safeStringify"; -import type { MembershipRole, Prisma } from "@calcom/prisma/client"; -import { RedirectType } from "@calcom/prisma/enums"; -import type { teamMetadataSchema } from "@calcom/prisma/zod-utils"; - -import { moveTeamToOrg, moveUserToOrg, removeTeamFromOrg, removeUserFromOrg } from "./orgMigration"; - -const WEBSITE_PROTOCOL = new URL(WEBSITE_URL).protocol; -describe("orgMigration", () => { - describe("moveUserToOrg", () => { - describe("when user email does not match orgAutoAcceptEmail", () => { - it(`should migrate a user to become a part of an organization with ADMIN role - - username in the organization should be automatically derived from email when it isn't passed`, async () => { - const data = { - userToMigrate: { - username: "user-1", - email: "user-1@example.com", - // Because example.com isn't the orgAutoAcceptEmail - expectedUsernameInOrg: "user-1-example", - }, - targetOrg: { - name: "Org 1", - slug: "org1", - }, - membershipWeWant: { - role: "ADMIN", - } as const, - }; - - const dbUserToMigrate = await createUserOutsideOrg({ - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }); - - const dbOrg = await createOrg({ - name: data.targetOrg.name, - slug: data.targetOrg.slug, - }); - - const team1 = await createTeamOutsideOrg({ - name: "Team-1", - slug: "team-1", - }); - - // Make userToMigrate part of team-1 - await addMemberShipOfUserWithTeam({ - teamId: team1.id, - userId: dbUserToMigrate.id, - role: "MEMBER", - accepted: true, - }); - - await moveUserToOrg({ - user: { - id: dbUserToMigrate.id, - }, - targetOrg: { - id: dbOrg.id, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: false, - }); - - await expectUserToBeAPartOfOrg({ - userId: dbUserToMigrate.id, - orgId: dbOrg.id, - usernameInOrg: data.userToMigrate.expectedUsernameInOrg, - expectedMembership: { role: data.membershipWeWant.role, accepted: true }, - }); - - await expectTeamToBeNotPartOfAnyOrganization({ - teamId: team1.id, - }); - - expectUserRedirectToBeEnabled({ - from: { - username: data.userToMigrate.username, - }, - to: data.userToMigrate.expectedUsernameInOrg, - orgSlug: data.targetOrg.slug, - }); - }); - - it("should migrate a user to become a part of an organization(which has slug set) with MEMBER role", async () => { - const data = { - userToMigrate: { - username: "user-1", - email: "user-1@example.com", - // Because example.com isn't the orgAutoAcceptEmail - expectedUsernameInOrg: "user-1-example", - }, - targetOrg: { - id: 1, - name: "Org 1", - slug: "org1", - }, - membershipWeWant: { - role: "MEMBER", - } as const, - }; - - const dbUserToMigrate = await createUserOutsideOrg({ - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }); - - const dbOrg = await createOrg({ - slug: data.targetOrg.slug, - name: data.targetOrg.name, - }); - - await moveUserToOrg({ - user: { - id: dbOrg.id, - }, - targetOrg: { - id: data.targetOrg.id, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: false, - }); - - await expectUserToBeAPartOfOrg({ - userId: dbUserToMigrate.id, - orgId: data.targetOrg.id, - usernameInOrg: data.userToMigrate.expectedUsernameInOrg, - expectedMembership: { role: data.membershipWeWant.role, accepted: true }, - }); - }); - - it(`should migrate a user to become a part of an organization(which has no slug but requestedSlug) with MEMBER role - 1. Should set the slug as requestedSlug for the organization(so that the redirect doesnt take to an unpublished organization page)`, async () => { - const data = { - userToMigrate: { - username: "user-1", - email: "user-1@example.com", - // Because example.com isn't the orgAutoAcceptEmail - expectedUsernameInOrg: "user-1-example", - }, - targetOrg: { - name: "Org 1", - requestedSlug: "org1", - }, - membershipWeWant: { - role: "MEMBER", - } as const, - }; - - const dbUserToMigrate = await createUserOutsideOrg({ - email: "user-1@example.com", - username: "user-1", - }); - - const dbOrg = await createOrg({ - name: data.targetOrg.name, - metadata: { - requestedSlug: data.targetOrg.requestedSlug, - }, - }); - - await moveUserToOrg({ - user: { - id: dbUserToMigrate.id, - }, - targetOrg: { - id: dbOrg.id, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: false, - }); - - const organization = await prismock.team.findUnique({ - where: { - id: dbOrg.id, - }, - }); - - expect(organization?.slug).toBe(data.targetOrg.requestedSlug); - - await expectUserToBeAPartOfOrg({ - userId: dbUserToMigrate.id, - orgId: dbOrg.id, - usernameInOrg: data.userToMigrate.expectedUsernameInOrg, - expectedMembership: { role: data.membershipWeWant.role, accepted: true }, - }); - }); - - it("should migrate a user along with its teams(without other members)", async () => { - const data = { - userToMigrate: { - username: "user-1", - email: "user-1@example.com", - // Because example.com isn't the orgAutoAcceptEmail - expectedUsernameInOrg: "user-1-example", - teams: [ - { - name: "Team 100", - slug: "team-100", - }, - { - name: "Team 101", - slug: "team-101", - }, - ], - }, - targetOrg: { - name: "Org 1", - slug: "org1", - }, - membershipWeWant: { - role: "MEMBER", - } as const, - userPartOfTeam1: { - username: "user-2", - email: "user-2@example.com", - }, - userPartOfTeam2: { - username: "user-3", - email: "user-3@example.com", - }, - }; - - // Create user to migrate - const dbUserToMigrate = await createUserOutsideOrg({ - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }); - - // Create another user that would be part of team-1 along with userToMigrate - const userPartOfTeam1 = await createUserOutsideOrg({ - email: data.userPartOfTeam1.email, - username: data.userPartOfTeam1.username, - }); - - // Create another user that would be part of team-2 along with userToMigrate - const userPartOfTeam2 = await createUserOutsideOrg({ - email: data.userPartOfTeam2.email, - username: data.userPartOfTeam2.username, - }); - - const team1 = await createTeamOutsideOrg({ - name: data.userToMigrate.teams[0].name, - slug: data.userToMigrate.teams[0].slug, - }); - - const team2 = await createTeamOutsideOrg({ - name: data.userToMigrate.teams[1].name, - slug: data.userToMigrate.teams[1].slug, - }); - - // Make userToMigrate part of team-1 - await addMemberShipOfUserWithTeam({ - teamId: team1.id, - userId: dbUserToMigrate.id, - role: "MEMBER", - accepted: true, - }); - - // Make userToMigrate part of team-2 - await addMemberShipOfUserWithTeam({ - teamId: team2.id, - userId: dbUserToMigrate.id, - role: "MEMBER", - accepted: true, - }); - - // Make userPartofTeam1 part of team-1 - await addMemberShipOfUserWithTeam({ - teamId: team1.id, - userId: userPartOfTeam1.id, - role: "MEMBER", - accepted: true, - }); - - // Make userPartofTeam2 part of team2 - await addMemberShipOfUserWithTeam({ - teamId: team2.id, - userId: userPartOfTeam2.id, - role: "MEMBER", - accepted: true, - }); - - const dbOrg = await createOrg({ - name: data.targetOrg.name, - slug: data.targetOrg.slug, - }); - - await moveUserToOrg({ - user: { - id: dbUserToMigrate.id, - }, - targetOrg: { - id: dbOrg.id, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: true, - }); - - await expectUserToBeAPartOfOrg({ - userId: dbUserToMigrate.id, - orgId: dbOrg.id, - usernameInOrg: data.userToMigrate.expectedUsernameInOrg, - expectedMembership: { role: data.membershipWeWant.role, accepted: true }, - }); - - await expectTeamToBeAPartOfOrg({ - teamId: team1.id, - orgId: dbOrg.id, - teamSlugInOrg: team1.slug, - }); - - await expectTeamToBeAPartOfOrg({ - teamId: team2.id, - orgId: dbOrg.id, - teamSlugInOrg: team2.slug, - }); - - await expectUserToBeNotAPartOfTheOrg({ - userId: userPartOfTeam1.id, - orgId: dbOrg.id, - username: data.userPartOfTeam1.username, - }); - - await expectUserToBeNotAPartOfTheOrg({ - userId: userPartOfTeam2.id, - orgId: dbOrg.id, - username: data.userPartOfTeam2.username, - }); - }); - - it(`should migrate a user to become a part of an organization - - username in the organization should same as provided to moveUserToOrg`, async () => { - const data = { - userToMigrate: { - username: "user-1", - email: "user-1@example.com", - usernameInOrgThatWeWant: "user-1-in-org", - }, - targetOrg: { - name: "Org 1", - slug: "org1", - }, - membershipWeWant: { - role: "ADMIN", - } as const, - }; - - const dbUserToMigrate = await createUserOutsideOrg({ - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }); - - const dbOrg = await createOrg({ - name: data.targetOrg.name, - slug: data.targetOrg.slug, - }); - - await moveUserToOrg({ - user: { - id: dbUserToMigrate.id, - }, - targetOrg: { - id: dbOrg.id, - username: data.userToMigrate.usernameInOrgThatWeWant, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: false, - }); - - await expectUserToBeAPartOfOrg({ - userId: dbUserToMigrate.id, - orgId: dbOrg.id, - usernameInOrg: data.userToMigrate.usernameInOrgThatWeWant, - expectedMembership: { role: data.membershipWeWant.role, accepted: true }, - }); - - expectUserRedirectToBeEnabled({ - from: { - username: data.userToMigrate.username, - }, - to: data.userToMigrate.usernameInOrgThatWeWant, - orgSlug: data.targetOrg.slug, - }); - }); - - it(`should be able to re-migrate an already migrated user fixing things as we do it - - Redirect should correctly determine the nonOrgUsername so that on repeat migrations, the original user link doesn't break`, async () => { - const data = { - userToMigrate: { - username: "user-1", - email: "user-1@example.com", - // Because example.com isn't the orgAutoAcceptEmail - usernameWeWantInOrg: "user-1-in-org", - }, - targetOrg: { - name: "Org 1", - slug: "org1", - }, - membershipWeWant: { - role: "MEMBER", - } as const, - }; - - const dbOrg = await createOrg({ - name: data.targetOrg.name, - slug: data.targetOrg.slug, - }); - - const dbUserToMigrate = await createUserInsideTheOrg( - { - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }, - dbOrg.id - ); - - await moveUserToOrg({ - user: { - id: dbUserToMigrate.id, - }, - targetOrg: { - id: dbOrg.id, - username: data.userToMigrate.usernameWeWantInOrg, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: false, - }); - - await expectUserToBeAPartOfOrg({ - userId: dbUserToMigrate.id, - orgId: dbOrg.id, - usernameInOrg: data.userToMigrate.usernameWeWantInOrg, - // membership role should be updated - expectedMembership: { role: data.membershipWeWant.role, accepted: true }, - }); - - await moveUserToOrg({ - user: { - id: dbUserToMigrate.id, - }, - targetOrg: { - id: dbOrg.id, - username: data.userToMigrate.usernameWeWantInOrg, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: false, - }); - - await expectUserToBeAPartOfOrg({ - userId: dbUserToMigrate.id, - orgId: dbOrg.id, - usernameInOrg: data.userToMigrate.usernameWeWantInOrg, - // membership role should be updated - expectedMembership: { role: data.membershipWeWant.role, accepted: true }, - }); - - expectUserRedirectToBeEnabled({ - from: { - username: data.userToMigrate.username, - }, - to: data.userToMigrate.usernameWeWantInOrg, - orgSlug: data.targetOrg.slug, - }); - - console.log(await prismock.tempOrgRedirect.findMany({})); - }); - - describe("Failures handling", () => { - it("migration should fail if the username already exists in the organization", async () => { - const data = { - userToMigrate: { - username: "user-1", - email: "user-1@example.com", - // Because example.com isn't the orgAutoAcceptEmail - expectedUsernameInOrg: "user-1-example", - }, - targetOrg: { - name: "Org 1", - slug: "org1", - }, - membershipWeWant: { - role: "MEMBER", - } as const, - }; - - const dbOrg = await createOrg({ - name: data.targetOrg.name, - slug: data.targetOrg.slug, - }); - - await createUserInsideTheOrg( - { - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }, - dbOrg.id - ); - - // User with same username exists outside the org as well as inside the org - const dbUserToMigrate = await createUserOutsideOrg({ - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }); - - expect(() => { - return moveUserToOrg({ - user: { - id: dbUserToMigrate.id, - }, - targetOrg: { - id: dbOrg.id, - username: data.userToMigrate.username, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: false, - }); - }).rejects.toThrowError("already exists"); - }); - - it("should fail the migration if the target org doesn't exist", async () => { - const data = { - userToMigrate: { - username: "user-1", - email: "user-1@example.com", - // Because example.com isn't the orgAutoAcceptEmail - expectedUsernameInOrg: "user-1-example", - }, - targetOrg: { - name: "Org 1", - slug: "org1", - }, - membershipWeWant: { - role: "MEMBER", - } as const, - }; - - const dbOrg = await createOrg({ - name: data.targetOrg.name, - slug: data.targetOrg.slug, - }); - - await createUserInsideTheOrg( - { - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }, - dbOrg.id - ); - - // User with same username exists outside the org as well as inside the org - const dbUserToMigrate = await createUserOutsideOrg({ - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }); - - expect(() => { - return moveUserToOrg({ - user: { - id: dbUserToMigrate.id, - }, - targetOrg: { - // Non existent teamId - id: 1001, - username: data.userToMigrate.username, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: false, - }); - }).rejects.toThrowError(/Org .* not found/); - }); - - it("should fail the migration if the target teamId is not an organization", async () => { - const data = { - userToMigrate: { - username: "user-1", - email: "user-1@example.com", - // Because example.com isn't the orgAutoAcceptEmail - expectedUsernameInOrg: "user-1-example", - }, - targetOrg: { - name: "Org 1", - slug: "org1", - }, - membershipWeWant: { - role: "MEMBER", - } as const, - }; - - // Create a team instead of an organization - const dbOrgWhichIsActuallyATeam = await createTeamOutsideOrg({ - name: data.targetOrg.name, - slug: data.targetOrg.slug, - }); - - const dbUserToMigrate = await createUserOutsideOrg({ - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }); - - expect(() => { - return moveUserToOrg({ - user: { - id: dbUserToMigrate.id, - }, - targetOrg: { - // Non existent teamId - id: dbOrgWhichIsActuallyATeam.id, - username: data.userToMigrate.username, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: false, - }); - }).rejects.toThrowError(/is not an Org/); - }); - - it("should fail the migration if the user is part of any other organization", async () => { - const data = { - userToMigrate: { - username: "user-1", - email: "user-1@example.com", - // Because example.com isn't the orgAutoAcceptEmail - expectedUsernameInOrg: "user-1-example", - }, - targetOrg: { - name: "Org 1", - slug: "org1", - }, - membershipWeWant: { - role: "MEMBER", - } as const, - }; - - const dbOrg = await createOrg({ - name: data.targetOrg.name, - slug: data.targetOrg.slug, - }); - - const dbOrgOther = await createOrg({ - name: data.targetOrg.name, - slug: data.targetOrg.slug, - }); - - const dbUserToMigrate = await createUserInsideTheOrg( - { - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }, - dbOrgOther.id - ); - - expect(() => { - return moveUserToOrg({ - user: { - id: dbUserToMigrate.id, - }, - targetOrg: { - // Non existent teamId - id: dbOrg.id, - username: data.userToMigrate.username, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: false, - }); - }).rejects.toThrowError(/already a part of different organization/); - }); - }); - }); - - describe("when user email matches orgAutoAcceptEmail", () => { - const orgSettings = { - orgAutoAcceptEmail: "org1.com", - } as const; - - it(`should migrate a user to become a part of an organization with ADMIN role`, async () => { - const data = { - userToMigrate: { - username: "user-1", - email: "user-1@org1.com", - // Because example.com isn't the orgAutoAcceptEmail - expectedUsernameInOrg: "user-1", - }, - targetOrg: { - name: "Org 1", - slug: "org1", - }, - membershipWeWant: { - role: "ADMIN", - } as const, - }; - - const dbUserToMigrate = await createUserOutsideOrg({ - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }); - - const dbOrg = await createOrg({ - slug: data.targetOrg.slug, - name: data.targetOrg.name, - organizationSettings: { - create: { - ...orgSettings, - }, - }, - }); - - await moveUserToOrg({ - user: { - id: dbUserToMigrate.id, - }, - targetOrg: { - id: dbOrg.id, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: false, - }); - - await expectUserToBeAPartOfOrg({ - userId: dbUserToMigrate.id, - orgId: dbOrg.id, - usernameInOrg: data.userToMigrate.expectedUsernameInOrg, - expectedMembership: { role: data.membershipWeWant.role, accepted: true }, - }); - - expectUserRedirectToBeEnabled({ - from: { - username: data.userToMigrate.username, - }, - to: data.userToMigrate.expectedUsernameInOrg, - orgSlug: data.targetOrg.slug, - }); - }); - - it("should migrate a user to become a part of an organization(which has slug set) with MEMBER role", async () => { - const data = { - userToMigrate: { - username: "user-1", - email: "user-1@org1.com", - // Because example.com isn't the orgAutoAcceptEmail - expectedUsernameInOrg: "user-1", - }, - targetOrg: { - name: "Org 1", - slug: "org1", - }, - membershipWeWant: { - role: "MEMBER", - } as const, - }; - - const dbUserToMigrate = await createUserOutsideOrg({ - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }); - - const dbOrg = await createOrg({ - slug: data.targetOrg.slug, - name: data.targetOrg.name, - organizationSettings: { - create: { - ...orgSettings, - }, - }, - }); - - await moveUserToOrg({ - user: { - id: dbUserToMigrate.id, - }, - targetOrg: { - id: dbOrg.id, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: false, - }); - - await expectUserToBeAPartOfOrg({ - userId: dbUserToMigrate.id, - orgId: dbOrg.id, - usernameInOrg: data.userToMigrate.expectedUsernameInOrg, - expectedMembership: { role: data.membershipWeWant.role, accepted: true }, - }); - }); - - it(`should migrate a user to become a part of an organization(which has no slug but requestedSlug) with MEMBER role - 1. Should set the slug as requestedSlug for the organization(so that the redirect doesnt take to an unpublished organization page)`, async () => { - const data = { - userToMigrate: { - username: "user-1", - email: "user-1@org1.com", - // Because example.com isn't the orgAutoAcceptEmail - expectedUsernameInOrg: "user-1", - }, - targetOrg: { - name: "Org 1", - requestedSlug: "org1", - }, - membershipWeWant: { - role: "MEMBER", - } as const, - }; - - const dbUserToMigrate = await createUserOutsideOrg({ - email: data.userToMigrate.email, - username: data.userToMigrate.username, - }); - - const dbOrg = await createOrg({ - name: data.targetOrg.name, - metadata: { - requestedSlug: data.targetOrg.requestedSlug, - }, - organizationSettings: { - create: { - ...orgSettings, - }, - }, - }); - - await moveUserToOrg({ - user: { - id: dbUserToMigrate.id, - }, - targetOrg: { - id: dbOrg.id, - membership: { - role: data.membershipWeWant.role, - }, - }, - shouldMoveTeams: false, - }); - - const organization = await prismock.team.findUnique({ - where: { - id: dbOrg.id, - }, - }); - - expect(organization?.slug).toBe(data.targetOrg.requestedSlug); - - await expectUserToBeAPartOfOrg({ - userId: dbUserToMigrate.id, - orgId: dbOrg.id, - usernameInOrg: data.userToMigrate.expectedUsernameInOrg, - expectedMembership: { role: data.membershipWeWant.role, accepted: true }, - }); - }); - }); - }); - - describe("moveTeamToOrg", () => { - it(`should migrate a team to become a part of an organization`, async () => { - const data = { - teamToMigrate: { - id: 1, - name: "Team 1", - slug: "team1", - newSlug: "team1-new-slug", - }, - targetOrg: { - id: 2, - name: "Org 1", - slug: "org1", - }, - }; - - await prismock.team.create({ - data: { - id: data.teamToMigrate.id, - slug: data.teamToMigrate.slug, - name: data.teamToMigrate.name, - }, - }); - - await prismock.team.create({ - data: { - id: data.targetOrg.id, - slug: data.targetOrg.slug, - name: data.targetOrg.name, - isOrganization: true, - }, - }); - - await moveTeamToOrg({ - teamId: data.teamToMigrate.id, - targetOrg: { - id: data.targetOrg.id, - teamSlug: data.teamToMigrate.newSlug, - }, - }); - - await expectTeamToBeAPartOfOrg({ - teamId: data.teamToMigrate.id, - orgId: data.targetOrg.id, - teamSlugInOrg: data.teamToMigrate.newSlug, - }); - - expectTeamRedirectToBeEnabled({ - from: { - teamSlug: data.teamToMigrate.slug, - }, - to: data.teamToMigrate.newSlug, - orgSlug: data.targetOrg.slug, - }); - }); - it.todo("should migrate a team with members"); - it.todo("Try migrating an already migrated team"); - }); - - describe("removeUserFromOrg", () => { - it(`should remove a user from an organization but he should remain in team`, async () => { - const data = { - userToUnmigrate: { - username: "user1-in-org1", - email: "user-1@example.com", - usernameBeforeMovingToOrg: "user1", - }, - targetOrg: { - name: "Org 1", - slug: "org1", - }, - membershipWeWant: { - role: "ADMIN", - } as const, - }; - - const dbOrg = await createOrg({ - slug: data.targetOrg.slug, - name: data.targetOrg.name, - }); - - const dbTeamOutsideOrg = await createTeamOutsideOrg({ - slug: "team-1", - name: "Team 1", - }); - - const dbUser = await createUserInsideTheOrg( - { - email: data.userToUnmigrate.email, - username: data.userToUnmigrate.username, - metadata: { - migratedToOrgFrom: { - username: data.userToUnmigrate.usernameBeforeMovingToOrg, - }, - }, - }, - dbOrg.id - ); - - await addMemberShipOfUserWithOrg({ - userId: dbUser.id, - teamId: dbTeamOutsideOrg.id, - role: "MEMBER", - accepted: true, - }); - - await addMemberShipOfUserWithTeam({ - userId: dbUser.id, - teamId: dbOrg.id, - role: data.membershipWeWant.role, - accepted: true, - }); - - const userToUnmigrate = await prismock.user.findUnique({ - where: { - id: dbUser.id, - }, - include: { - organization: true, - }, - }); - - if (!userToUnmigrate?.organizationId || !userToUnmigrate.organization) { - throw new Error( - `Couldn't setup user to unmigrate properly userToUnMigrate: ${{ - organizationId: userToUnmigrate?.organizationId, - organization: !!userToUnmigrate?.organization, - }}` - ); - } - - await removeUserFromOrg({ - userId: dbUser.id, - targetOrgId: dbOrg.id, - }); - - await expectUserToBeNotAPartOfTheOrg({ - userId: dbUser.id, - orgId: dbOrg.id, - username: data.userToUnmigrate.usernameBeforeMovingToOrg, - }); - - await expectUserToBeAPartOfTeam({ - userId: dbUser.id, - teamId: dbTeamOutsideOrg.id, - expectedMembership: { - role: "MEMBER", - accepted: true, - }, - }); - - expectUserRedirectToBeNotEnabled({ - from: { - username: data.userToUnmigrate.username, - }, - }); - }); - }); - - describe("removeTeamFromOrg", () => { - it(`should remove a team from an organization`, async () => { - const data = { - teamToUnmigrate: { - name: "Team 1", - slug: "team1", - }, - targetOrg: { - name: "Org 1", - slug: "org1", - }, - }; - - const targetOrg = await prismock.team.create({ - data: { - slug: data.targetOrg.slug, - name: data.targetOrg.name, - metadata: { - isOrganization: true, - }, - }, - }); - - const { id: teamToUnMigrateId } = await prismock.team.create({ - data: { - slug: data.teamToUnmigrate.slug, - name: data.teamToUnmigrate.name, - parent: { - connect: { - id: targetOrg.id, - }, - }, - }, - }); - - const teamToUnmigrate = await prismock.team.findUnique({ - where: { - id: teamToUnMigrateId, - }, - include: { - parent: true, - }, - }); - - if (!teamToUnmigrate?.parent || !teamToUnmigrate.parentId) { - throw new Error(`Couldn't setup team to unmigrate properly ID:${teamToUnMigrateId}`); - } - - await removeTeamFromOrg({ - teamId: teamToUnMigrateId, - targetOrgId: targetOrg.id, - }); - - await expectTeamToBeNotPartOfAnyOrganization({ - teamId: teamToUnMigrateId, - }); - - expectTeamRedirectToBeNotEnabled({ - from: { - teamSlug: data.teamToUnmigrate.slug, - }, - to: data.teamToUnmigrate.slug, - orgSlug: data.targetOrg.slug, - }); - }); - - it(`should remove a team from an organization with it's members`, async () => { - const data = { - teamToUnmigrate: { - name: "Team 1", - slug: "team1", - members: [ - { - username: "user1-example", - email: "user1@example.com", - usernameBeforeMovingToOrg: "user1", - }, - ], - }, - targetOrg: { - name: "Org 1", - slug: "org1", - }, - }; - - const targetOrg = await prismock.team.create({ - data: { - slug: data.targetOrg.slug, - name: data.targetOrg.name, - metadata: { - isOrganization: true, - }, - }, - }); - - const { id: teamToUnMigrateId } = await prismock.team.create({ - data: { - slug: data.teamToUnmigrate.slug, - name: data.teamToUnmigrate.name, - parent: { - connect: { - id: targetOrg.id, - }, - }, - }, - }); - - const teamToUnmigrate = await prismock.team.findUnique({ - where: { - id: teamToUnMigrateId, - }, - include: { - parent: true, - }, - }); - - if (!teamToUnmigrate?.parent || !teamToUnmigrate.parentId) { - throw new Error(`Couldn't setup team to unmigrate properly ID:${teamToUnMigrateId}`); - } - - const member1OfTeam = await createUserInsideTheOrg( - { - email: data.teamToUnmigrate.members[0].email, - username: data.teamToUnmigrate.members[0].username, - metadata: { - migratedToOrgFrom: { - username: data.teamToUnmigrate.members[0].usernameBeforeMovingToOrg, - }, - }, - }, - targetOrg.id - ); - - addMemberShipOfUserWithOrg({ - userId: member1OfTeam.id, - teamId: targetOrg.id, - role: "MEMBER", - accepted: true, - }); - - addMemberShipOfUserWithTeam({ - teamId: teamToUnmigrate.id, - userId: member1OfTeam.id, - role: "MEMBER", - accepted: true, - }); - - expectUserToBeAPartOfOrg({ - userId: member1OfTeam.id, - orgId: targetOrg.id, - usernameInOrg: data.teamToUnmigrate.members[0].username, - expectedMembership: { - role: "MEMBER", - accepted: true, - }, - }); - - expectUserToBeAPartOfTeam({ - userId: member1OfTeam.id, - teamId: teamToUnmigrate.id, - expectedMembership: { - role: "MEMBER", - accepted: true, - }, - }); - - await removeTeamFromOrg({ - teamId: teamToUnMigrateId, - targetOrgId: targetOrg.id, - }); - - await expectTeamToBeNotPartOfAnyOrganization({ - teamId: teamToUnMigrateId, - }); - - expectTeamRedirectToBeNotEnabled({ - from: { - teamSlug: data.teamToUnmigrate.slug, - }, - to: data.teamToUnmigrate.slug, - orgSlug: data.targetOrg.slug, - }); - - expectUserRedirectToBeNotEnabled({ - from: { - username: data.teamToUnmigrate.members[0].usernameBeforeMovingToOrg, - }, - }); - - expectUserToBeNotAPartOfTheOrg({ - userId: member1OfTeam.id, - orgId: targetOrg.id, - username: data.teamToUnmigrate.members[0].usernameBeforeMovingToOrg, - }); - - expectUserToBeNotAPartOfTheTeam({ - userId: member1OfTeam.id, - teamId: targetOrg.id, - username: data.teamToUnmigrate.members[0].usernameBeforeMovingToOrg, - }); - }); - }); -}); - -async function expectUserToBeAPartOfOrg({ - userId, - orgId, - expectedMembership, - usernameInOrg, -}: { - userId: number; - orgId: number; - usernameInOrg: string; - expectedMembership: { - role: MembershipRole; - accepted: boolean; - }; -}) { - const migratedUser = await prismock.user.findUnique({ - where: { - id: userId, - }, - include: { - teams: true, - }, - }); - if (!migratedUser) { - throw new Error(`User with id ${userId} does not exist`); - } - - expect(migratedUser.username).toBe(usernameInOrg); - expect(migratedUser.organizationId).toBe(orgId); - - const membership = migratedUser.teams.find( - (membership) => membership.teamId === orgId && membership.userId === userId - ); - - expect(membership).not.toBeNull(); - if (!membership) { - throw new Error(`User with id ${userId} is not a part of org with id ${orgId}`); - } - expect(membership.role).toBe(expectedMembership.role); - expect(membership.accepted).toBe(expectedMembership.accepted); -} - -async function expectUserToBeAPartOfTeam({ - userId, - teamId, - expectedMembership, -}: { - userId: number; - teamId: number; - expectedMembership: { - role: MembershipRole; - accepted: boolean; - }; -}) { - const user = await prismock.user.findUnique({ - where: { - id: userId, - }, - include: { - teams: true, - }, - }); - if (!user) { - throw new Error(`User with id ${userId} does not exist`); - } - - const membership = user.teams.find( - (membership) => membership.teamId === teamId && membership.userId === userId - ); - - expect(membership).not.toBeNull(); - if (!membership) { - throw new Error(`User with id ${userId} is not a part of team with id ${teamId}`); - } - expect(membership.role).toBe(expectedMembership.role); - expect(membership.accepted).toBe(expectedMembership.accepted); -} - -async function expectUserToBeNotAPartOfTheOrg({ - userId, - orgId, - username, -}: { - userId: number; - orgId: number; - username: string; -}) { - expectUserToBeNotAPartOfTheTeam({ - userId, - teamId: orgId, - username, - }); -} - -async function expectUserToBeNotAPartOfTheTeam({ - userId, - teamId, - username, -}: { - userId: number; - teamId: number; - username: string; -}) { - const user = await prismock.user.findUnique({ - where: { - id: userId, - }, - include: { - teams: true, - }, - }); - if (!user) { - throw new Error(`User with id ${userId} does not exist`); - } - - expect(user.username).toBe(username); - expect(user.organizationId).toBe(null); - - const membership = user.teams.find( - (membership) => membership.teamId === teamId && membership.userId === userId - ); - - expect(membership).toBeUndefined(); -} - -async function expectTeamToBeAPartOfOrg({ - teamId, - orgId, - teamSlugInOrg, -}: { - teamId: number; - orgId: number; - teamSlugInOrg: string | null; -}) { - const migratedTeam = await prismock.team.findUnique({ - where: { - id: teamId, - }, - }); - if (!migratedTeam) { - throw new Error(`Team with id ${teamId} does not exist`); - } - - if (!teamSlugInOrg) { - throw new Error(`teamSlugInOrg should be defined`); - } - expect(migratedTeam.parentId).toBe(orgId); - expect(migratedTeam.slug).toBe(teamSlugInOrg); -} - -async function expectTeamToBeNotPartOfAnyOrganization({ teamId }: { teamId: number }) { - const team = await prismock.team.findUnique({ - where: { - id: teamId, - }, - }); - if (!team) { - throw new Error(`Team with id ${teamId} does not exist`); - } - - expect(team.parentId).toBe(null); -} - -async function expectUserRedirectToBeEnabled({ - from, - to, - orgSlug, -}: { - from: { username: string } | { teamSlug: string }; - to: string; - orgSlug: string; -}) { - expectRedirectToBeEnabled({ - from, - to, - orgSlug, - redirectType: RedirectType.User, - }); -} - -async function expectTeamRedirectToBeEnabled({ - from, - to, - orgSlug, -}: { - from: { username: string } | { teamSlug: string }; - to: string; - orgSlug: string; -}) { - expectRedirectToBeEnabled({ - from, - to, - orgSlug, - redirectType: RedirectType.Team, - }); -} - -async function expectUserRedirectToBeNotEnabled({ - from, -}: { - from: { username: string } | { teamSlug: string }; -}) { - expectRedirectToBeNotEnabled({ - from, - }); -} - -async function expectTeamRedirectToBeNotEnabled({ - from, -}: { - from: { username: string } | { teamSlug: string }; - to: string; - orgSlug: string; -}) { - expectRedirectToBeNotEnabled({ - from, - }); -} - -async function expectRedirectToBeEnabled({ - from, - to, - orgSlug, - redirectType, -}: { - from: { username: string } | { teamSlug: string }; - to: string; - orgSlug: string; - redirectType: RedirectType; -}) { - let tempOrgRedirectWhere = null; - let tempOrgRedirectThatShouldNotExistWhere = null; - if ("username" in from) { - tempOrgRedirectWhere = { - from_type_fromOrgId: { - from: from.username, - type: RedirectType.User, - fromOrgId: 0, - }, - }; - - // Normally with user migration `from.username != to` - if (from.username !== to) { - // There must not be a redirect setup from=To to something else as that would cause double redirection - tempOrgRedirectThatShouldNotExistWhere = { - from_type_fromOrgId: { - from: to, - type: RedirectType.User, - fromOrgId: 0, - }, - }; - } - } else if ("teamSlug" in from) { - tempOrgRedirectWhere = { - from_type_fromOrgId: { - from: from.teamSlug, - type: RedirectType.Team, - fromOrgId: 0, - }, - }; - - if (from.teamSlug !== to) { - // There must not be a redirect setup from=To to something else as that would cause double redirection - tempOrgRedirectThatShouldNotExistWhere = { - from_type_fromOrgId: { - from: to, - type: RedirectType.Team, - fromOrgId: 0, - }, - }; - } - } else { - throw new Error("Atleast one of userId or teamId should be present in from"); - } - const redirect = await prismock.tempOrgRedirect.findUnique({ - where: tempOrgRedirectWhere, - }); - - if (tempOrgRedirectThatShouldNotExistWhere) { - const redirectThatShouldntBeThere = await prismock.tempOrgRedirect.findUnique({ - where: tempOrgRedirectThatShouldNotExistWhere, - }); - expect(redirectThatShouldntBeThere).toBeNull(); - } - - expect(redirect).not.toBeNull(); - expect(redirect?.toUrl).toBe(`${WEBSITE_PROTOCOL}//${orgSlug}.cal.local:3000/${to}`); - if (!redirect) { - throw new Error(`Redirect doesn't exist for ${JSON.stringify(tempOrgRedirectWhere)}`); - } - expect(redirect.type).toBe(redirectType); -} - -async function expectRedirectToBeNotEnabled({ from }: { from: { username: string } | { teamSlug: string } }) { - let tempOrgRedirectWhere = null; - if ("username" in from) { - tempOrgRedirectWhere = { - from_type_fromOrgId: { - from: from.username, - type: RedirectType.User, - fromOrgId: 0, - }, - }; - } else if ("teamSlug" in from) { - tempOrgRedirectWhere = { - from_type_fromOrgId: { - from: from.teamSlug, - type: RedirectType.Team, - fromOrgId: 0, - }, - }; - } else { - throw new Error("Atleast one of userId or teamId should be present in from"); - } - const redirect = await prismock.tempOrgRedirect.findUnique({ - where: tempOrgRedirectWhere, - }); - expect(redirect).toBeNull(); -} - -async function createOrg( - data: Omit & { - metadata?: z.infer; - } -) { - return await prismock.team.create({ - data: { - ...data, - isOrganization: true, - metadata: { - ...(data.metadata || {}), - }, - createdByOAuthClient: undefined, - }, - }); -} - -async function createTeamOutsideOrg( - data: Omit & { - metadata?: z.infer; - } -) { - return await prismock.team.create({ - data: { - ...data, - parentId: null, - metadata: { - ...(data.metadata || {}), - isOrganization: false, - }, - createdByOAuthClient: undefined, - }, - }); -} - -async function createUserOutsideOrg( - data: Omit -) { - return await prismock.user.create({ - data: { - ...data, - movedToProfileId: null, - organizationId: null, - }, - }); -} - -async function createUserInsideTheOrg( - data: Omit, - orgId: number -) { - const org = await prismock.team.findUnique({ - where: { - id: orgId, - }, - }); - if (!org) { - throw new Error(`Org with id ${orgId} does not exist`); - } - logger.debug( - `Creating user inside org`, - safeStringify({ - orgId, - data, - }) - ); - return await prismock.user.create({ - data: { - ...data, - organization: { - connect: { - id: orgId, - }, - }, - }, - }); -} - -async function addMemberShipOfUserWithTeam({ - teamId, - userId, - role, - accepted, -}: { - teamId: number; - userId: number; - role: MembershipRole; - accepted: boolean; -}) { - await prismock.membership.create({ - data: { - role, - accepted, - team: { - connect: { - id: teamId, - }, - }, - user: { - connect: { - id: userId, - }, - }, - }, - }); - - const membership = await prismock.membership.findUnique({ - where: { - userId_teamId: { - teamId, - userId, - }, - }, - }); - if (!membership) { - throw new Error(`Membership between teamId ${teamId} and userId ${userId} couldn't be created`); - } -} - -const addMemberShipOfUserWithOrg = addMemberShipOfUserWithTeam; diff --git a/apps/web/pages/api/orgMigration/moveTeamToOrg.ts b/apps/web/pages/api/orgMigration/moveTeamToOrg.ts deleted file mode 100644 index f6a3e2bec4..0000000000 --- a/apps/web/pages/api/orgMigration/moveTeamToOrg.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { getFormSchema } from "@pages/settings/admin/orgMigrations/moveTeamToOrg"; -import type { NextApiRequest, NextApiResponse } from "next/types"; - -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { HttpError } from "@calcom/lib/http-error"; -import logger from "@calcom/lib/logger"; -import { safeStringify } from "@calcom/lib/safeStringify"; -import { getTranslation } from "@calcom/lib/server"; -import { UserPermissionRole } from "@calcom/prisma/enums"; - -import { moveTeamToOrg } from "../../../lib/orgMigration"; - -const log = logger.getSubLogger({ prefix: ["moveTeamToOrg"] }); - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const rawBody = req.body; - - log.debug( - "Moving team to org:", - safeStringify({ - body: rawBody, - }) - ); - - const translate = await getTranslation("en", "common"); - const moveTeamToOrgSchema = getFormSchema(translate); - - const parsedBody = moveTeamToOrgSchema.safeParse(rawBody); - - const session = await getServerSession({ req, res }); - - if (!session) { - return res.status(403).json({ message: "No session found" }); - } - - const isAdmin = session.user.role === UserPermissionRole.ADMIN; - - if (!parsedBody.success) { - log.error("moveTeamToOrg failed:", safeStringify(parsedBody.error)); - return res.status(400).json({ message: JSON.stringify(parsedBody.error) }); - } - - const { teamId, targetOrgId, moveMembers, teamSlugInOrganization } = parsedBody.data; - const isAllowed = isAdmin; - if (!isAllowed) { - return res.status(403).json({ message: "Not Authorized" }); - } - - try { - await moveTeamToOrg({ - targetOrg: { - id: targetOrgId, - teamSlug: teamSlugInOrganization, - }, - teamId, - moveMembers, - }); - } catch (error) { - if (error instanceof HttpError) { - if (error.statusCode > 300) { - log.error("moveTeamToOrg failed:", safeStringify(error.message)); - } - return res.status(error.statusCode).json({ message: error.message }); - } - log.error("moveTeamToOrg failed:", safeStringify(error)); - const errorMessage = error instanceof Error ? error.message : "Something went wrong"; - - return res.status(500).json({ message: errorMessage }); - } - - return res.status(200).json({ - message: `Added team ${teamId} to Org: ${targetOrgId} ${ - moveMembers ? " along with the members" : " without the members" - }`, - }); -} diff --git a/apps/web/pages/api/orgMigration/moveUserToOrg.ts b/apps/web/pages/api/orgMigration/moveUserToOrg.ts deleted file mode 100644 index 82b299c5b8..0000000000 --- a/apps/web/pages/api/orgMigration/moveUserToOrg.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { getFormSchema } from "@pages/settings/admin/orgMigrations/moveUserToOrg"; -import type { NextApiRequest, NextApiResponse } from "next/types"; - -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { HttpError } from "@calcom/lib/http-error"; -import logger from "@calcom/lib/logger"; -import { safeStringify } from "@calcom/lib/safeStringify"; -import { getTranslation } from "@calcom/lib/server"; -import { UserPermissionRole } from "@calcom/prisma/enums"; - -import { moveUserToOrg } from "../../../lib/orgMigration"; - -const log = logger.getSubLogger({ prefix: ["moveUserToOrg"] }); - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const rawBody = req.body; - const translate = await getTranslation("en", "common"); - const migrateBodySchema = getFormSchema(translate); - log.debug( - "Starting migration:", - safeStringify({ - body: rawBody, - }) - ); - const parsedBody = migrateBodySchema.safeParse(rawBody); - - const session = await getServerSession({ req }); - - if (!session) { - res.status(403).json({ message: "No session found" }); - return; - } - - const isAdmin = session.user.role === UserPermissionRole.ADMIN; - - if (parsedBody.success) { - const { userId, userName, shouldMoveTeams, targetOrgId, targetOrgUsername, targetOrgRole } = - parsedBody.data; - const isAllowed = isAdmin; - if (isAllowed) { - try { - await moveUserToOrg({ - targetOrg: { - id: targetOrgId, - username: targetOrgUsername, - membership: { - role: targetOrgRole, - }, - }, - user: { - id: userId, - userName, - }, - shouldMoveTeams, - }); - } catch (error) { - if (error instanceof HttpError) { - if (error.statusCode > 300) { - log.error("Migration failed:", safeStringify(error)); - } - return res.status(error.statusCode).json({ message: error.message }); - } - log.error("Migration failed:", safeStringify(error)); - const errorMessage = error instanceof Error ? error.message : "Something went wrong"; - - return res.status(400).json({ message: errorMessage }); - } - } else { - return res.status(403).json({ message: "Not Authorized" }); - } - return res.status(200).json({ message: "Migrated" }); - } - log.error("Migration failed:", safeStringify(parsedBody.error)); - return res.status(400).json({ message: JSON.stringify(parsedBody.error) }); -} diff --git a/apps/web/pages/api/orgMigration/removeTeamFromOrg.ts b/apps/web/pages/api/orgMigration/removeTeamFromOrg.ts deleted file mode 100644 index fc4a88e4bb..0000000000 --- a/apps/web/pages/api/orgMigration/removeTeamFromOrg.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { getFormSchema } from "@pages/settings/admin/orgMigrations/removeTeamFromOrg"; -import type { NextApiRequest, NextApiResponse } from "next/types"; - -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { HttpError } from "@calcom/lib/http-error"; -import logger from "@calcom/lib/logger"; -import { safeStringify } from "@calcom/lib/safeStringify"; -import { getTranslation } from "@calcom/lib/server"; -import { UserPermissionRole } from "@calcom/prisma/enums"; - -import { removeTeamFromOrg } from "../../../lib/orgMigration"; - -const log = logger.getSubLogger({ prefix: ["removeTeamFromOrg"] }); - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const rawBody = req.body; - const translate = await getTranslation("en", "common"); - const removeTeamFromOrgSchema = getFormSchema(translate); - log.debug( - "Removing team from org:", - safeStringify({ - body: rawBody, - }) - ); - const parsedBody = removeTeamFromOrgSchema.safeParse(rawBody); - - const session = await getServerSession({ req }); - - if (!session) { - return res.status(403).json({ message: "No session found" }); - } - - const isAdmin = session.user.role === UserPermissionRole.ADMIN; - - if (!parsedBody.success) { - log.error("RemoveTeamFromOrg failed:", safeStringify(parsedBody.error)); - return res.status(400).json({ message: JSON.stringify(parsedBody.error) }); - } - const { teamId, targetOrgId } = parsedBody.data; - const isAllowed = isAdmin; - if (!isAllowed) { - return res.status(403).json({ message: "Not Authorized" }); - } - - try { - await removeTeamFromOrg({ - targetOrgId, - teamId, - }); - } catch (error) { - if (error instanceof HttpError) { - if (error.statusCode > 300) { - log.error("RemoveTeamFromOrg failed:", safeStringify(error)); - } - return res.status(error.statusCode).json({ message: error.message }); - } - log.error("RemoveTeamFromOrg failed:", safeStringify(error)); - const errorMessage = error instanceof Error ? error.message : "Something went wrong"; - return res.status(500).json({ message: errorMessage }); - } - - return res.status(200).json({ message: `Removed team ${teamId} from ${targetOrgId}` }); -} diff --git a/apps/web/pages/api/orgMigration/removeUserFromOrg.ts b/apps/web/pages/api/orgMigration/removeUserFromOrg.ts deleted file mode 100644 index ce48fc07e9..0000000000 --- a/apps/web/pages/api/orgMigration/removeUserFromOrg.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { getFormSchema } from "@pages/settings/admin/orgMigrations/removeUserFromOrg"; -import type { NextApiRequest, NextApiResponse } from "next/types"; - -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { HttpError } from "@calcom/lib/http-error"; -import logger from "@calcom/lib/logger"; -import { safeStringify } from "@calcom/lib/safeStringify"; -import { getTranslation } from "@calcom/lib/server"; -import { UserPermissionRole } from "@calcom/prisma/enums"; - -import { removeUserFromOrg } from "../../../lib/orgMigration"; - -const log = logger.getSubLogger({ prefix: ["removeUserFromOrg"] }); - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const body = req.body; - - log.debug( - "Starting reverse migration:", - safeStringify({ - body, - }) - ); - - const translate = await getTranslation("en", "common"); - const migrateRevertBodySchema = getFormSchema(translate); - const parsedBody = migrateRevertBodySchema.safeParse(body); - const session = await getServerSession({ req }); - - if (!session) { - return res.status(403).json({ message: "No session found" }); - } - - const isAdmin = session.user.role === UserPermissionRole.ADMIN; - if (!isAdmin) { - return res.status(403).json({ message: "Only admin can take this action" }); - } - - if (parsedBody.success) { - const { userId, targetOrgId } = parsedBody.data; - try { - await removeUserFromOrg({ targetOrgId, userId }); - } catch (error) { - if (error instanceof HttpError) { - if (error.statusCode > 300) { - log.error("Reverse migration failed:", safeStringify(error)); - } - return res.status(error.statusCode).json({ message: error.message }); - } - log.error("Reverse migration failed:", safeStringify(error)); - const errorMessage = error instanceof Error ? error.message : "Something went wrong"; - - return res.status(500).json({ message: errorMessage }); - } - return res.status(200).json({ message: "Reverted" }); - } - log.error("Reverse Migration failed:", safeStringify(parsedBody.error)); - return res.status(400).json({ message: JSON.stringify(parsedBody.error) }); -} diff --git a/apps/web/pages/settings/admin/orgMigrations/_OrgMigrationLayout.tsx b/apps/web/pages/settings/admin/orgMigrations/_OrgMigrationLayout.tsx deleted file mode 100644 index cf51a4a792..0000000000 --- a/apps/web/pages/settings/admin/orgMigrations/_OrgMigrationLayout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { getLayout as getSettingsLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import { HorizontalTabs } from "@calcom/ui"; - -export default function OrgMigrationLayout({ children }: { children: React.ReactElement }) { - return getSettingsLayout( -
- - {children} -
- ); -} -export const getLayout = (page: React.ReactElement) => { - return {page}; -}; diff --git a/apps/web/pages/settings/admin/orgMigrations/moveTeamToOrg.tsx b/apps/web/pages/settings/admin/orgMigrations/moveTeamToOrg.tsx deleted file mode 100644 index fdcd246642..0000000000 --- a/apps/web/pages/settings/admin/orgMigrations/moveTeamToOrg.tsx +++ /dev/null @@ -1,153 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import type { TFunction } from "next-i18next"; -import { useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { z } from "zod"; - -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { getStringAsNumberRequiredSchema } from "@calcom/prisma/zod-utils"; -import { Button, Form, Meta, SelectField, TextField, showToast } from "@calcom/ui"; - -import { getServerSideProps } from "@lib/settings/admin/orgMigrations/moveTeamToOrg/getServerSideProps"; - -import PageWrapper from "@components/PageWrapper"; - -import { getLayout } from "./_OrgMigrationLayout"; - -export { getServerSideProps }; - -export const getFormSchema = (t: TFunction) => { - return z.object({ - teamId: z.number().or(getStringAsNumberRequiredSchema(t)), - targetOrgId: z.number().or(getStringAsNumberRequiredSchema(t)), - moveMembers: z.boolean(), - teamSlugInOrganization: z.string(), - }); -}; - -function Wrapper({ children }: { children: React.ReactNode }) { - return ( -
- - {children} -
- ); -} - -const enum State { - IDLE, - LOADING, - SUCCESS, - ERROR, -} - -export default function MoveTeamToOrg() { - const [state, setState] = useState(State.IDLE); - const moveUsersOptions = [ - { - label: "No", - value: "false", - }, - { - label: "Yes", - value: "true", - }, - ]; - const { t } = useLocale(); - const formSchema = getFormSchema(t); - const formMethods = useForm({ - mode: "onSubmit", - resolver: zodResolver(formSchema), - }); - - const { register, watch } = formMethods; - const moveMembers = watch("moveMembers"); - return ( - -
{ - setState(State.LOADING); - const res = await fetch(`/api/orgMigration/moveTeamToOrg`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(values), - }); - let response = null; - try { - response = await res.json(); - } catch (e) { - if (e instanceof Error) { - showToast(e.message, "error", 10000); - } else { - showToast(t("something_went_wrong"), "error", 10000); - } - setState(State.ERROR); - return; - } - if (res.status === 200) { - setState(State.SUCCESS); - showToast(response.message, "success", 10000); - } else { - setState(State.ERROR); - showToast(response.message, "error", 10000); - } - }}> -
- - - -
- ( - { - onChange(option?.value === "true"); - }} - value={moveUsersOptions.find((opt) => opt.value === value)} - options={moveUsersOptions} - /> - )} - /> - - {moveMembers === true ? ( -
Members of the team will also be moved to the organization
- ) : moveMembers === false ? ( -
Members of the team will not be moved to the organization
- ) : null} -
-
- -
-
- ); -} - -MoveTeamToOrg.PageWrapper = PageWrapper; -MoveTeamToOrg.getLayout = getLayout; diff --git a/apps/web/pages/settings/admin/orgMigrations/moveUserToOrg.tsx b/apps/web/pages/settings/admin/orgMigrations/moveUserToOrg.tsx deleted file mode 100644 index 495ed57586..0000000000 --- a/apps/web/pages/settings/admin/orgMigrations/moveUserToOrg.tsx +++ /dev/null @@ -1,188 +0,0 @@ -"use client"; - -/** - * It could be an admin feature to move a user to an organization but because it's a temporary thing before mono-user orgs are implemented, it's not right to spend time on it. - * Plus, we need to do it only for cal.com and not provide as a feature to our self hosters. - */ -import { zodResolver } from "@hookform/resolvers/zod"; -import type { TFunction } from "next-i18next"; -import { useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import z from "zod"; - -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { MembershipRole } from "@calcom/prisma/client"; -import { getStringAsNumberRequiredSchema } from "@calcom/prisma/zod-utils"; -import { Button, Form, Meta, SelectField, TextField, showToast } from "@calcom/ui"; - -import { getServerSideProps } from "@lib/settings/admin/orgMigrations/moveUserToOrg/getServerSideProps"; - -import PageWrapper from "@components/PageWrapper"; - -import { getLayout } from "./_OrgMigrationLayout"; - -export { getServerSideProps }; - -function Wrapper({ children }: { children: React.ReactNode }) { - return ( -
- - {children} -
- ); -} - -export const getFormSchema = (t: TFunction) => - z.object({ - userId: z.union([z.string().pipe(z.coerce.number()), z.number()]).optional(), - userName: z.string().optional(), - targetOrgId: z.union([getStringAsNumberRequiredSchema(t), z.number()]), - targetOrgUsername: z.string().min(1, t("error_required_field")), - shouldMoveTeams: z.boolean(), - targetOrgRole: z.union([ - z.literal(MembershipRole.ADMIN), - z.literal(MembershipRole.MEMBER), - z.literal(MembershipRole.OWNER), - ]), - }); - -const enum State { - IDLE, - LOADING, - SUCCESS, - ERROR, -} -export default function MoveUserToOrg() { - const [state, setState] = useState(State.IDLE); - - const roles = Object.values(MembershipRole).map((role) => ({ - label: role, - value: role, - })); - - const moveTeamsOptions = [ - { - label: "Yes", - value: "true", - }, - { - label: "No", - value: "false", - }, - ]; - const { t } = useLocale(); - const formSchema = getFormSchema(t); - const form = useForm({ - mode: "onSubmit", - resolver: zodResolver(formSchema), - }); - - const shouldMoveTeams = form.watch("shouldMoveTeams"); - const register = form.register; - return ( - - {/* Due to some reason auth from website doesn't work if /api endpoint is used. Spent a lot of time and in the end went with submitting data to the same page, because you can't do POST request to a page in Next.js, doing a GET request */} -
{ - setState(State.LOADING); - const res = await fetch(`/api/orgMigration/moveUserToOrg`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(values), - }); - let response = null; - try { - response = await res.json(); - } catch (e) { - if (e instanceof Error) { - showToast(e.message, "error", 10000); - } else { - showToast(t("something_went_wrong"), "error", 10000); - } - setState(State.ERROR); - return; - } - if (res.status === 200) { - setState(State.SUCCESS); - showToast(response.message, "success", 10000); - } else { - setState(State.ERROR); - showToast(response.message, "error", 10000); - } - }}> -
- - ( - { - if (!option) return; - onChange(option.value); - }} - value={roles.find((role) => role.value === value)} - required - placeholder="Enter userId" - /> - )} - /> - - - ( - { - if (!option) return; - onChange(option.value === "true"); - }} - value={moveTeamsOptions.find((opt) => opt.value === value)} - required - options={moveTeamsOptions} - /> - )} - /> -
- - -
-
- ); -} - -MoveUserToOrg.PageWrapper = PageWrapper; -MoveUserToOrg.getLayout = getLayout; diff --git a/apps/web/pages/settings/admin/orgMigrations/removeTeamFromOrg.tsx b/apps/web/pages/settings/admin/orgMigrations/removeTeamFromOrg.tsx deleted file mode 100644 index 2148457abd..0000000000 --- a/apps/web/pages/settings/admin/orgMigrations/removeTeamFromOrg.tsx +++ /dev/null @@ -1,117 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import type { TFunction } from "next-i18next"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import z from "zod"; - -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { getStringAsNumberRequiredSchema } from "@calcom/prisma/zod-utils"; -import { Button, Form, Meta, TextField, showToast } from "@calcom/ui"; - -import { getServerSideProps } from "@lib/settings/admin/orgMigrations/removeTeamFromOrg/getServerSideProps"; - -import PageWrapper from "@components/PageWrapper"; - -import { getLayout } from "./_OrgMigrationLayout"; - -export { getServerSideProps }; - -function Wrapper({ children }: { children: React.ReactNode }) { - return ( -
- - {children} -
- ); -} - -const enum State { - IDLE, - LOADING, - SUCCESS, - ERROR, -} - -export const getFormSchema = (t: TFunction) => - z.object({ - targetOrgId: z.union([getStringAsNumberRequiredSchema(t), z.number()]), - teamId: z.union([getStringAsNumberRequiredSchema(t), z.number()]), - }); - -export default function RemoveTeamFromOrg() { - const [state, setState] = useState(State.IDLE); - const { t } = useLocale(); - const formSchema = getFormSchema(t); - const form = useForm({ - mode: "onSubmit", - resolver: zodResolver(formSchema), - }); - - const register = form.register; - - return ( - - {/* Due to some reason auth from website doesn't work if /api endpoint is used. Spent a lot of time and in the end went with submitting data to the same page, because you can't do POST request to a page in Next.js, doing a GET request */} -
{ - setState(State.LOADING); - const res = await fetch(`/api/orgMigration/removeTeamFromOrg`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(values), - }); - let response = null; - try { - response = await res.json(); - } catch (e) { - if (e instanceof Error) { - showToast(e.message, "error", 10000); - } else { - showToast(t("something_went_wrong"), "error", 10000); - } - setState(State.ERROR); - return; - } - if (res.status === 200) { - setState(State.SUCCESS); - showToast(response.message, "success", 10000); - } else { - setState(State.ERROR); - showToast(response.message, "error", 10000); - } - }}> -
- - -
- -
-
- ); -} - -RemoveTeamFromOrg.PageWrapper = PageWrapper; -RemoveTeamFromOrg.getLayout = getLayout; diff --git a/apps/web/pages/settings/admin/orgMigrations/removeUserFromOrg.tsx b/apps/web/pages/settings/admin/orgMigrations/removeUserFromOrg.tsx deleted file mode 100644 index 18d18a15d8..0000000000 --- a/apps/web/pages/settings/admin/orgMigrations/removeUserFromOrg.tsx +++ /dev/null @@ -1,115 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import type { TFunction } from "next-i18next"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import z from "zod"; - -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { getStringAsNumberRequiredSchema } from "@calcom/prisma/zod-utils"; -import { Button, TextField, Meta, showToast, Form } from "@calcom/ui"; - -import { getServerSideProps } from "@lib/settings/admin/orgMigrations/removeUserFromOrg/getServerSideProps"; - -import PageWrapper from "@components/PageWrapper"; - -import { getLayout } from "./_OrgMigrationLayout"; - -export { getServerSideProps }; - -function Wrapper({ children }: { children: React.ReactNode }) { - return ( -
- - {children} -
- ); -} - -const enum State { - IDLE, - LOADING, - SUCCESS, - ERROR, -} - -export const getFormSchema = (t: TFunction) => - z.object({ - userId: z.union([getStringAsNumberRequiredSchema(t), z.number()]), - targetOrgId: z.union([getStringAsNumberRequiredSchema(t), z.number()]), - }); - -export default function RemoveUserFromOrg() { - const [state, setState] = useState(State.IDLE); - const { t } = useLocale(); - const formSchema = getFormSchema(t); - const form = useForm({ - mode: "onSubmit", - resolver: zodResolver(formSchema), - }); - const register = form.register; - return ( - - {/* Due to some reason auth from website doesn't work if /api endpoint is used. Spent a lot of time and in the end went with submitting data to the same page, because you can't do POST request to a page in Next.js, doing a GET request */} -
{ - setState(State.LOADING); - const res = await fetch(`/api/orgMigration/removeUserFromOrg`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(values), - }); - let response = null; - try { - response = await res.json(); - } catch (e) { - if (e instanceof Error) { - showToast(e.message, "error", 10000); - } else { - showToast(t("something_went_wrong"), "error", 10000); - } - setState(State.ERROR); - return; - } - if (res.status === 200) { - setState(State.SUCCESS); - showToast(response.message, "success", 10000); - } else { - setState(State.ERROR); - showToast(response.message, "error", 10000); - } - }}> -
- - -
- -
-
- ); -} - -RemoveUserFromOrg.PageWrapper = PageWrapper; -RemoveUserFromOrg.getLayout = getLayout; diff --git a/apps/web/playwright/change-username.e2e.ts b/apps/web/playwright/change-username.e2e.ts index 658ab33b51..2225746195 100644 --- a/apps/web/playwright/change-username.e2e.ts +++ b/apps/web/playwright/change-username.e2e.ts @@ -4,9 +4,8 @@ import stripe from "@calcom/features/ee/payments/server/stripe"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { MembershipRole } from "@calcom/prisma/enums"; -import { moveUserToOrg } from "@lib/orgMigration"; - import { test } from "./lib/fixtures"; +import { moveUserToOrg } from "./lib/orgMigration"; import { IS_STRIPE_ENABLED, submitAndWaitForResponse } from "./lib/testUtils"; test.describe.configure({ mode: "parallel" }); diff --git a/apps/web/lib/orgMigration.ts b/apps/web/playwright/lib/orgMigration.ts similarity index 81% rename from apps/web/lib/orgMigration.ts rename to apps/web/playwright/lib/orgMigration.ts index 68d8863af1..a119892206 100644 --- a/apps/web/lib/orgMigration.ts +++ b/apps/web/playwright/lib/orgMigration.ts @@ -179,122 +179,6 @@ export async function removeUserFromOrg({ targetOrgId, userId }: { targetOrgId: log.debug(`orgId:${targetOrgId} attached to userId:${userId}`); } -/** - * Make sure that the migration is idempotent - */ -export async function moveTeamToOrg({ - targetOrg, - teamId, - moveMembers, -}: { - targetOrg: { id: number; teamSlug: string }; - teamId: number; - moveMembers?: boolean; -}) { - const possibleOrg = await getTeamOrThrowError(targetOrg.id); - const { oldTeamSlug, updatedTeam } = await dbMoveTeamToOrg({ teamId, targetOrg }); - - const teamMetadata = teamMetadataSchema.parse(possibleOrg?.metadata); - - if (!possibleOrg.isOrganization) { - throw new Error(`${targetOrg.id} is not an Org`); - } - - const targetOrganization = possibleOrg; - const orgMetadata = teamMetadata; - await addTeamRedirect({ - oldTeamSlug, - teamSlug: updatedTeam.slug, - orgSlug: targetOrganization.slug || orgMetadata?.requestedSlug || null, - }); - await setOrgSlugIfNotSet({ slug: targetOrganization.slug }, orgMetadata, targetOrg.id); - if (moveMembers) { - for (const membership of updatedTeam.members) { - await moveUserToOrg({ - user: { - id: membership.userId, - }, - targetOrg: { - id: targetOrg.id, - membership: { - role: membership.role, - accepted: membership.accepted, - }, - }, - shouldMoveTeams: false, - }); - } - } - log.debug(`Successfully moved team ${teamId} to org ${targetOrg.id}`); -} - -/** - * Make sure that the migration is idempotent - */ -export async function removeTeamFromOrg({ targetOrgId, teamId }: { targetOrgId: number; teamId: number }) { - const removedTeam = await dbRemoveTeamFromOrg({ teamId }); - await removeTeamRedirect(removedTeam.slug); - for (const membership of removedTeam.members) { - await removeUserFromOrg({ - userId: membership.userId, - targetOrgId, - }); - } - log.debug(`Successfully removed team ${teamId} from org ${targetOrgId}`); -} - -async function dbMoveTeamToOrg({ - teamId, - targetOrg, -}: { - teamId: number; - targetOrg: { - id: number; - teamSlug: string; - }; -}) { - const team = await prisma.team.findUnique({ - where: { - id: teamId, - }, - include: { - members: true, - }, - }); - - if (!team) { - throw new HttpError({ - statusCode: 400, - message: `Team with id: ${teamId} not found`, - }); - } - - const teamMetadata = teamMetadataSchema.parse(team?.metadata); - const oldTeamSlug = teamMetadata?.migratedToOrgFrom?.teamSlug || team.slug; - - const updatedTeam = await prisma.team.update({ - where: { - id: teamId, - }, - data: { - slug: targetOrg.teamSlug, - parentId: targetOrg.id, - metadata: { - ...teamMetadata, - migratedToOrgFrom: { - teamSlug: team.slug, - lastMigrationTime: new Date().toISOString(), - }, - }, - }, - include: { - members: true, - }, - }); - - return { oldTeamSlug, updatedTeam }; -} - async function getUniqueUserThatDoesntBelongToOrg( userName: string | undefined, userId: number | undefined, @@ -491,53 +375,6 @@ async function addRedirect({ } } -async function addTeamRedirect({ - oldTeamSlug, - teamSlug, - orgSlug, -}: { - oldTeamSlug: string | null; - teamSlug: string | null; - orgSlug: string | null; -}) { - if (!oldTeamSlug) { - throw new HttpError({ - statusCode: 400, - message: "No oldSlug for team. Not adding the redirect", - }); - } - if (!teamSlug) { - throw new HttpError({ - statusCode: 400, - message: "No slug for team. Not adding the redirect", - }); - } - if (!orgSlug) { - log.warn(`No slug for org. Not adding the redirect`); - return; - } - const orgUrlPrefix = getOrgFullOrigin(orgSlug); - - await prisma.tempOrgRedirect.upsert({ - where: { - from_type_fromOrgId: { - type: RedirectType.Team, - from: oldTeamSlug, - fromOrgId: 0, - }, - }, - create: { - type: RedirectType.Team, - from: oldTeamSlug, - fromOrgId: 0, - toUrl: `${orgUrlPrefix}/${teamSlug}`, - }, - update: { - toUrl: `${orgUrlPrefix}/${teamSlug}`, - }, - }); -} - async function updateMembership({ targetOrgId, userToMoveToOrg, @@ -700,24 +537,6 @@ async function setOrgSlug({ targetOrgId, targetSlug }: { targetOrgId: number; ta }); } -async function removeTeamRedirect(teamSlug: string | null) { - if (!teamSlug) { - throw new HttpError({ - statusCode: 400, - message: "No slug for team. Not removing the redirect", - }); - return; - } - - await prisma.tempOrgRedirect.deleteMany({ - where: { - type: RedirectType.Team, - from: teamSlug, - fromOrgId: 0, - }, - }); -} - async function removeUserAlongWithItsTeamsRedirects({ nonOrgUserName, teamsToBeRemovedFromOrg, diff --git a/apps/web/playwright/organization/organization-invitation.e2e.ts b/apps/web/playwright/organization/organization-invitation.e2e.ts index c244d23f8d..a9604e2bc4 100644 --- a/apps/web/playwright/organization/organization-invitation.e2e.ts +++ b/apps/web/playwright/organization/organization-invitation.e2e.ts @@ -5,9 +5,8 @@ import prisma from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/enums"; -import { moveUserToOrg } from "@lib/orgMigration"; - import { test } from "../lib/fixtures"; +import { moveUserToOrg } from "../lib/orgMigration"; import { bookTeamEvent, doOnOrgDomain, expectPageToBeNotFound, getInviteLink } from "../lib/testUtils"; import { expectInvitationEmailToBeReceived } from "./expects"; diff --git a/packages/features/settings/layouts/SettingsLayout.tsx b/packages/features/settings/layouts/SettingsLayout.tsx deleted file mode 100644 index 757f543f08..0000000000 --- a/packages/features/settings/layouts/SettingsLayout.tsx +++ /dev/null @@ -1,797 +0,0 @@ -"use client"; - -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; -import { useSession } from "next-auth/react"; -import Link from "next/link"; -import { usePathname, useRouter } from "next/navigation"; -import type { ComponentProps } from "react"; -import React, { Suspense, useEffect, useState, useMemo } from "react"; - -import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; -import Shell from "@calcom/features/shell/Shell"; -import { classNames } from "@calcom/lib"; -import { HOSTED_CAL_FEATURES, IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants"; -import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; -import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; -import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { IdentityProvider, MembershipRole, UserPermissionRole } from "@calcom/prisma/enums"; -import { trpc } from "@calcom/trpc/react"; -import type { VerticalTabItemProps } from "@calcom/ui"; -import { Badge, Button, ErrorBoundary, Icon, Skeleton, useMeta, VerticalTabItem } from "@calcom/ui"; - -const tabs: VerticalTabItemProps[] = [ - { - name: "my_account", - href: "/settings/my-account", - icon: "user", - children: [ - { name: "profile", href: "/settings/my-account/profile" }, - { name: "general", href: "/settings/my-account/general" }, - { name: "calendars", href: "/settings/my-account/calendars" }, - { name: "conferencing", href: "/settings/my-account/conferencing" }, - { name: "appearance", href: "/settings/my-account/appearance" }, - { name: "out_of_office", href: "/settings/my-account/out-of-office" }, - // TODO - // { name: "referrals", href: "/settings/my-account/referrals" }, - ], - }, - { - name: "security", - href: "/settings/security", - icon: "key", - children: [ - { name: "password", href: "/settings/security/password" }, - { name: "impersonation", href: "/settings/security/impersonation" }, - { name: "2fa_auth", href: "/settings/security/two-factor-auth" }, - ], - }, - { - name: "billing", - href: "/settings/billing", - icon: "credit-card", - children: [{ name: "manage_billing", href: "/settings/billing" }], - }, - { - name: "developer", - href: "/settings/developer", - icon: "terminal", - children: [ - // - { name: "webhooks", href: "/settings/developer/webhooks" }, - { name: "api_keys", href: "/settings/developer/api-keys" }, - { name: "admin_api", href: "/settings/organizations/admin-api" }, - // TODO: Add profile level for embeds - // { name: "embeds", href: "/v2/settings/developer/embeds" }, - ], - }, - { - name: "organization", - href: "/settings/organizations", - children: [ - { - name: "profile", - href: "/settings/organizations/profile", - }, - { - name: "general", - href: "/settings/organizations/general", - }, - { - name: "privacy", - href: "/settings/organizations/privacy", - }, - { - name: "billing", - href: "/settings/organizations/billing", - }, - { name: "OAuth Clients", href: "/settings/organizations/platform/oauth-clients" }, - { - name: "SSO", - href: "/settings/organizations/sso", - }, - { - name: "directory_sync", - href: "/settings/organizations/dsync", - }, - { - name: "admin_api", - href: "https://cal.com/docs/enterprise-features/api/api-reference/bookings#admin-access", - }, - // { - // name: "domain_wide_delegation", - // href: "/settings/organizations/domain-wide-delegation", - // }, - ], - }, - { - name: "teams", - href: "/teams", - icon: "users", - children: [], - }, - { - name: "other_teams", - href: "/settings/organizations/teams/other", - icon: "users", - children: [], - }, - { - name: "admin", - href: "/settings/admin", - icon: "lock", - children: [ - // - { name: "features", href: "/settings/admin/flags" }, - { name: "license", href: "/auth/setup?step=1" }, - { name: "impersonation", href: "/settings/admin/impersonation" }, - { name: "apps", href: "/settings/admin/apps/calendar" }, - { name: "users", href: "/settings/admin/users" }, - { name: "organizations", href: "/settings/admin/organizations" }, - { name: "lockedSMS", href: "/settings/admin/lockedSMS" }, - { name: "oAuth", href: "/settings/admin/oAuth" }, - { name: "Workspace Platforms", href: "/settings/admin/workspace-platforms" }, - ], - }, -]; - -tabs.find((tab) => { - if (tab.name === "security" && !HOSTED_CAL_FEATURES) { - tab.children?.push({ name: "sso_configuration", href: "/settings/security/sso" }); - // TODO: Enable dsync for self hosters - // tab.children?.push({ name: "directory_sync", href: "/settings/security/dsync" }); - } - if (tab.name === "admin" && IS_CALCOM) { - tab.children?.push({ name: "create_your_org", href: "/settings/organizations/new" }); - } - if (tab.name === "admin" && IS_CALCOM) { - tab.children?.push({ name: "create_license_key", href: "/settings/license-key/new" }); - } -}); - -// The following keys are assigned to admin only -const adminRequiredKeys = ["admin"]; -const organizationRequiredKeys = ["organization"]; -const organizationAdminKeys = ["privacy", "billing", "OAuth Clients", "SSO", "directory_sync"]; - -const useTabs = () => { - const session = useSession(); - const { data: user } = trpc.viewer.me.useQuery({ includePasswordAdded: true }); - const orgBranding = useOrgBranding(); - const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN; - const isOrgAdminOrOwner = - orgBranding?.role === MembershipRole.ADMIN || orgBranding?.role === MembershipRole.OWNER; - // const flags = useFlags(); - // console.log("flags", flags); - - const processTabsMemod = useMemo(() => { - const processedTabs = tabs.map((tab) => { - if (tab.href === "/settings/my-account") { - return { - ...tab, - name: user?.name || "my_account", - icon: undefined, - avatar: getUserAvatarUrl(user), - }; - } else if (tab.href === "/settings/organizations") { - const newArray = (tab?.children ?? []).filter( - (child) => isOrgAdminOrOwner || !organizationAdminKeys.includes(child.name) - ); - - // TODO: figure out feature flag as it doesnt cause a re-render of the component when loaded. - // You have to refresh the page to see the changes. - if (true) { - newArray.splice(4, 0, { - name: "attributes", - href: "/settings/organizations/attributes", - }); - } - - return { - ...tab, - children: newArray, - name: orgBranding?.name || "organization", - avatar: getPlaceholderAvatar(orgBranding?.logoUrl, orgBranding?.name), - }; - } else if ( - tab.href === "/settings/security" && - user?.identityProvider === IdentityProvider.GOOGLE && - !user?.twoFactorEnabled && - !user?.passwordAdded - ) { - const filtered = tab?.children?.filter( - (childTab) => childTab.href !== "/settings/security/two-factor-auth" - ); - return { ...tab, children: filtered }; - } else if (tab.href === "/settings/developer") { - const filtered = tab?.children?.filter( - (childTab) => isOrgAdminOrOwner || childTab.name !== "admin_api" - ); - return { ...tab, children: filtered }; - } - return tab; - }); - - // check if name is in adminRequiredKeys - return processedTabs.filter((tab) => { - if (organizationRequiredKeys.includes(tab.name)) return !!orgBranding; - if (tab.name === "other_teams" && !isOrgAdminOrOwner) return false; - - if (isAdmin) return true; - return !adminRequiredKeys.includes(tab.name); - }); - }, [isAdmin, orgBranding, isOrgAdminOrOwner, user]); - - return processTabsMemod; -}; - -const BackButtonInSidebar = ({ name }: { name: string }) => { - return ( - - - - {name} - - - ); -}; - -interface SettingsSidebarContainerProps { - className?: string; - navigationIsOpenedOnMobile?: boolean; - bannersHeight?: number; -} - -const TeamListCollapsible = () => { - const { data: teams } = trpc.viewer.teams.list.useQuery(); - const { t } = useLocale(); - const [teamMenuState, setTeamMenuState] = - useState<{ teamId: number | undefined; teamMenuOpen: boolean }[]>(); - const searchParams = useCompatSearchParams(); - useEffect(() => { - if (teams) { - const teamStates = teams?.map((team) => ({ - teamId: team.id, - teamMenuOpen: String(team.id) === searchParams?.get("id"), - })); - setTeamMenuState(teamStates); - setTimeout(() => { - const tabMembers = Array.from(document.getElementsByTagName("a")).filter( - (bottom) => bottom.dataset.testid === "vertical-tab-Members" - )[1]; - // eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- Settings layout isn't embedded - tabMembers?.scrollIntoView({ behavior: "smooth" }); - }, 100); - } - }, [searchParams?.get("id"), teams]); - - return ( - <> - {teams && - teamMenuState && - teams.map((team, index: number) => { - if (!teamMenuState[index]) { - return null; - } - if (teamMenuState.some((teamState) => teamState.teamId === team.id)) - return ( - - setTeamMenuState([ - ...teamMenuState, - (teamMenuState[index] = { - ...teamMenuState[index], - teamMenuOpen: !teamMenuState[index].teamMenuOpen, - }), - ]) - }> - -
- setTeamMenuState([ - ...teamMenuState, - (teamMenuState[index] = { - ...teamMenuState[index], - teamMenuOpen: !teamMenuState[index].teamMenuOpen, - }), - ]) - }> -
- {teamMenuState[index].teamMenuOpen ? ( - - ) : ( - - )} -
- {!team.parentId && ( - {team.name - )} -

{team.name}

- {!team.accepted && ( - - Inv. - - )} -
-
- - {team.accepted && ( - - )} - - - {(team.role === MembershipRole.OWNER || - team.role === MembershipRole.ADMIN || - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore this exists wtf? - (team.isOrgAdmin && team.isOrgAdmin)) && ( - <> - {/* TODO */} - {/* */} - - {/* Hide if there is a parent ID */} - {!team.parentId ? ( - <> - - - ) : null} - - - )} - -
- ); - })} - - ); -}; - -const SettingsSidebarContainer = ({ - className = "", - navigationIsOpenedOnMobile, - bannersHeight, -}: SettingsSidebarContainerProps) => { - const searchParams = useCompatSearchParams(); - const { t } = useLocale(); - const tabsWithPermissions = useTabs(); - const [otherTeamMenuState, setOtherTeamMenuState] = useState< - { - teamId: number | undefined; - teamMenuOpen: boolean; - }[] - >(); - const session = useSession(); - const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { - enabled: !!session.data?.user?.org, - }); - - const { data: otherTeams } = trpc.viewer.organizations.listOtherTeams.useQuery(undefined, { - enabled: !!session.data?.user?.org, - }); - - // Same as above but for otherTeams - useEffect(() => { - if (otherTeams) { - const otherTeamStates = otherTeams?.map((team) => ({ - teamId: team.id, - teamMenuOpen: String(team.id) === searchParams?.get("id"), - })); - setOtherTeamMenuState(otherTeamStates); - setTimeout(() => { - // @TODO: test if this works for 2 dataset testids - const tabMembers = Array.from(document.getElementsByTagName("a")).filter( - (bottom) => bottom.dataset.testid === "vertical-tab-Members" - )[1]; - // eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- Settings layout isn't embedded - tabMembers?.scrollIntoView({ behavior: "smooth" }); - }, 100); - } - }, [searchParams?.get("id"), otherTeams]); - - const isOrgAdminOrOwner = - currentOrg && currentOrg?.user?.role && ["OWNER", "ADMIN"].includes(currentOrg?.user?.role); - - return ( - - ); -}; - -const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) => { - const { t } = useLocale(); - const router = useRouter(); - - return ( - <> - - - ); -}; - -export type SettingsLayoutProps = { - children: React.ReactNode; - hideHeader?: boolean; - containerClassName?: string; -} & ComponentProps; - -export default function SettingsLayout({ children, hideHeader, ...rest }: SettingsLayoutProps) { - const pathname = usePathname(); - const state = useState(false); - const { t } = useLocale(); - const [sideContainerOpen, setSideContainerOpen] = state; - - useEffect(() => { - const closeSideContainer = () => { - if (window.innerWidth >= 1024) { - setSideContainerOpen(false); - } - }; - - window.addEventListener("resize", closeSideContainer); - return () => { - window.removeEventListener("resize", closeSideContainer); - }; - }, []); - - useEffect(() => { - if (sideContainerOpen) { - setSideContainerOpen(!sideContainerOpen); - } - }, [pathname]); - - return ( - - } - drawerState={state} - MobileNavigationContainer={null} - TopNavContainer={ - setSideContainerOpen(!sideContainerOpen)} /> - }> -
-
- {!hideHeader && } - - }>{children} - -
-
-
- ); -} - -const SidebarContainerElement = ({ - sideContainerOpen, - bannersHeight, - setSideContainerOpen, -}: SidebarContainerElementProps) => { - const { t } = useLocale(); - return ( - <> - {/* Mobile backdrop */} - {sideContainerOpen && ( - - )} - - - ); -}; - -type SidebarContainerElementProps = { - sideContainerOpen: boolean; - bannersHeight?: number; - setSideContainerOpen: React.Dispatch>; -}; - -export const getLayout = (page: React.ReactElement) => {page}; - -export function ShellHeader() { - const { meta } = useMeta(); - const { t, isLocaleReady } = useLocale(); - return ( - <> -
-
- {meta.backButton && ( - - - - )} -
- {meta.title && isLocaleReady ? ( -

- {t(meta.title)} -

- ) : ( -
- )} - {meta.description && isLocaleReady ? ( -

{t(meta.description)}

- ) : ( -
- )} -
-
{meta.CTA}
-
-
- - ); -}