dc43eba23b
* feat: restore moveTeamToOrg admin endpoint for organization migration - Add moveTeamToOrg and removeTeamFromOrg functions to orgMigration.ts - Restore API endpoint at /api/orgMigration/moveTeamToOrg - Restore admin UI page at /settings/admin/orgMigrations/moveTeamToOrg - Add helper functions for team redirect management - Support moving team members along with the team This endpoint allows admins to migrate teams to organizations after org creation, which is needed as a temporary solution until proper org admin permissions are implemented. Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: move moveTeamToOrg to lib/orgMigration and fix redirect URL - Move moveTeamToOrg and removeTeamFromOrg functions from playwright/lib to lib/orgMigration.ts - Update API endpoint to import from lib/orgMigration instead of playwright/lib - Fix redirect URL format: use / instead of /team/ - Fix import path: use ../playwright/lib/orgMigration instead of ./playwright/lib/orgMigration - Rename unused _dbRemoveTeamFromOrg in playwright file to satisfy linter - Remove duplicate functions from playwright/lib/orgMigration.ts This fixes the Vercel deployment failure caused by importing from test-only directories in production API routes, and corrects the redirect URL format to match the original implementation. Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * refactor: reuse existing createTeamsHandler for moveTeamToOrg endpoint - Remove custom orgMigration.ts implementation - Update API endpoint to call existing createTeamsHandler with org owner impersonation - Remove moveMembers option from UI (always moves members by design) - Fix Vercel deployment by removing playwright import from production code - Use OrganizationRepository.adminFindById to fetch org owner Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * refactor: migrate moveTeamToOrg admin page and API to App Router - Move admin page from pages/settings/admin/orgMigrations to app/(use-page-wrapper)/settings/(admin-layout)/admin/orgMigrations - Convert API route from pages/api/orgMigration/moveTeamToOrg.ts to app/api/orgMigration/moveTeamToOrg/route.ts - Create client view component in modules/settings/admin/org-migrations/ - Remove old pages directory files and getServerSideProps Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: correct import paths for App Router compatibility - Fix @calcom/lib/server to @calcom/lib/server/i18n for getTranslation - Fix @calcom/ui barrel import to specific component paths Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: use TFunction type for getFormSchema parameter Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: use buildLegacyRequest for App Router session compatibility Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Remove unused fn * cleanup * cleanup * fix: ui * fix: handle slug conflict error when moving team to organization - Intercept Prisma P2002 unique constraint error when moving a team - Convert to user-friendly CONFLICT error with clear message - Add test case for slug conflict scenario Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: use isPending instead of isLoading for tRPC mutation Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fixes * fix: remove PII (emails) from admin log statement Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * refactor: use instanceof pattern for Prisma error detection Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fixes * fix: use i18n key for slug conflict error message instead of hardcoded English string Co-Authored-By: bot_apk <apk@cognition.ai> * fix: narrow P2002 catch scope to only prisma.team.update call Separates the try-catch for prisma.team.update (slug conflict) from creditService.moveCreditsFromTeamToOrg to avoid misattributing credit service P2002 errors as slug conflicts. Co-Authored-By: bot_apk <apk@cognition.ai> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: bot_apk <apk@cognition.ai>
673 lines
17 KiB
TypeScript
673 lines
17 KiB
TypeScript
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
|
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
|
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
|
|
import { HttpError } from "@calcom/lib/http-error";
|
|
import logger from "@calcom/lib/logger";
|
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
|
import prisma from "@calcom/prisma";
|
|
import type { Team, User } from "@calcom/prisma/client";
|
|
import { Prisma } from "@calcom/prisma/client";
|
|
import type { MembershipRole } from "@calcom/prisma/enums";
|
|
import { RedirectType } from "@calcom/prisma/enums";
|
|
import { teamMetadataSchema, teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
|
|
|
|
const log = logger.getSubLogger({ prefix: ["orgMigration"] });
|
|
|
|
type UserMetadata = {
|
|
migratedToOrgFrom?: {
|
|
username: string;
|
|
reverted: boolean;
|
|
revertTime: string;
|
|
lastMigrationTime: string;
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Make sure that the migration is idempotent
|
|
*/
|
|
export async function moveUserToOrg({
|
|
user: { id: userId, userName },
|
|
targetOrg: {
|
|
id: targetOrgId,
|
|
username: targetOrgUsername,
|
|
membership: { role: targetOrgRole, accepted: targetOrgMembershipAccepted = true },
|
|
},
|
|
shouldMoveTeams,
|
|
}: {
|
|
user: { id?: number; userName?: string };
|
|
targetOrg: {
|
|
id: number;
|
|
username?: string;
|
|
membership: { role: MembershipRole; accepted?: boolean };
|
|
};
|
|
shouldMoveTeams: boolean;
|
|
}) {
|
|
assertUserIdOrUserName(userId, userName);
|
|
const team = await getTeamOrThrowError(targetOrgId);
|
|
|
|
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
|
|
|
|
if (!team.isOrganization) {
|
|
throw new Error(`Team with ID:${targetOrgId} is not an Org`);
|
|
}
|
|
|
|
const targetOrganization = {
|
|
...team,
|
|
metadata: teamMetadata,
|
|
};
|
|
const userToMoveToOrg = await getUniqueUserThatDoesntBelongToOrg(userName, userId, targetOrgId);
|
|
assertUserPartOfOtherOrg(userToMoveToOrg, userName, userId, targetOrgId);
|
|
|
|
if (!targetOrgUsername) {
|
|
targetOrgUsername = getOrgUsernameFromEmail(
|
|
userToMoveToOrg.email,
|
|
targetOrganization.organizationSettings?.orgAutoAcceptEmail || ""
|
|
);
|
|
}
|
|
|
|
const userWithSameUsernameInOrg = await prisma.user.findUnique({
|
|
where: {
|
|
username_organizationId: {
|
|
username: targetOrgUsername,
|
|
organizationId: targetOrgId,
|
|
},
|
|
},
|
|
});
|
|
|
|
log.debug({
|
|
userWithSameUsernameInOrg,
|
|
targetOrgUsername,
|
|
targetOrgId,
|
|
userId,
|
|
});
|
|
|
|
if (userWithSameUsernameInOrg && userWithSameUsernameInOrg.id !== userId) {
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: `Username ${targetOrgUsername} already exists for orgId: ${targetOrgId} for some other user`,
|
|
});
|
|
}
|
|
|
|
assertUserPartOfOrgAndRemigrationAllowed(userToMoveToOrg, targetOrgId, targetOrgUsername, userId);
|
|
|
|
const orgMetadata = teamMetadata;
|
|
|
|
const userToMoveToOrgMetadata = (userToMoveToOrg.metadata || {}) as UserMetadata;
|
|
|
|
const nonOrgUserName =
|
|
(userToMoveToOrgMetadata.migratedToOrgFrom?.username as string) || userToMoveToOrg.username;
|
|
if (!nonOrgUserName) {
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: `User with id: ${userId} doesn't have a non-org username`,
|
|
});
|
|
}
|
|
|
|
await dbMoveUserToOrg({ userToMoveToOrg, targetOrgId, targetOrgUsername, nonOrgUserName });
|
|
|
|
let teamsToBeMovedToOrg;
|
|
if (shouldMoveTeams) {
|
|
teamsToBeMovedToOrg = await moveTeamsWithoutMembersToOrg({ targetOrgId, userToMoveToOrg });
|
|
}
|
|
|
|
await updateMembership({ targetOrgId, userToMoveToOrg, targetOrgRole, targetOrgMembershipAccepted });
|
|
|
|
await addRedirect({
|
|
nonOrgUserName,
|
|
teamsToBeMovedToOrg: teamsToBeMovedToOrg || [],
|
|
organization: targetOrganization,
|
|
targetOrgUsername,
|
|
});
|
|
|
|
await setOrgSlugIfNotSet(targetOrganization, orgMetadata, targetOrgId);
|
|
|
|
log.debug(`orgId:${targetOrgId} attached to userId:${userId}`);
|
|
}
|
|
|
|
/**
|
|
* Make sure that the migration is idempotent
|
|
*/
|
|
export async function removeUserFromOrg({ targetOrgId, userId }: { targetOrgId: number; userId: number }) {
|
|
const userToRemoveFromOrg = await prisma.user.findUnique({
|
|
where: {
|
|
id: userId,
|
|
},
|
|
});
|
|
|
|
if (!userToRemoveFromOrg) {
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: `User with id: ${userId} not found`,
|
|
});
|
|
}
|
|
|
|
if (userToRemoveFromOrg.organizationId !== targetOrgId) {
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: `User with id: ${userId} is not part of orgId: ${targetOrgId}`,
|
|
});
|
|
}
|
|
|
|
const userToRemoveFromOrgMetadata = (userToRemoveFromOrg.metadata || {}) as {
|
|
migratedToOrgFrom?: {
|
|
username: string;
|
|
reverted: boolean;
|
|
revertTime: string;
|
|
lastMigrationTime: string;
|
|
};
|
|
};
|
|
|
|
if (!userToRemoveFromOrgMetadata.migratedToOrgFrom) {
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: `User with id: ${userId} wasn't migrated. So, there is nothing to revert`,
|
|
});
|
|
}
|
|
|
|
const nonOrgUserName = userToRemoveFromOrgMetadata.migratedToOrgFrom.username as string;
|
|
if (!nonOrgUserName) {
|
|
throw new HttpError({
|
|
statusCode: 500,
|
|
message: `User with id: ${userId} doesn't have a non-org username`,
|
|
});
|
|
}
|
|
|
|
const teamsToBeRemovedFromOrg = await removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg });
|
|
await dbRemoveUserFromOrg({ userToRemoveFromOrg, nonOrgUserName });
|
|
|
|
await removeUserAlongWithItsTeamsRedirects({ nonOrgUserName, teamsToBeRemovedFromOrg });
|
|
await removeMembership({ targetOrgId, userToRemoveFromOrg });
|
|
|
|
log.debug(`orgId:${targetOrgId} attached to userId:${userId}`);
|
|
}
|
|
|
|
async function getUniqueUserThatDoesntBelongToOrg(
|
|
userName: string | undefined,
|
|
userId: number | undefined,
|
|
excludeOrgId: number
|
|
) {
|
|
log.debug("getUniqueUserThatDoesntBelongToOrg", { userName, userId, excludeOrgId });
|
|
if (userName) {
|
|
const matchingUsers = await prisma.user.findMany({
|
|
where: {
|
|
username: userName,
|
|
},
|
|
});
|
|
const foundUsers = matchingUsers.filter(
|
|
(user) => user.organizationId === excludeOrgId || user.organizationId === null
|
|
);
|
|
if (foundUsers.length > 1) {
|
|
throw new Error(`More than one user found with username: ${userName}`);
|
|
}
|
|
return foundUsers[0];
|
|
} else {
|
|
return await prisma.user.findUnique({
|
|
where: {
|
|
id: userId,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
async function setOrgSlugIfNotSet(
|
|
targetOrganization: {
|
|
slug: string | null;
|
|
},
|
|
orgMetadata: {
|
|
requestedSlug?: string | null | undefined;
|
|
} | null,
|
|
targetOrgId: number
|
|
) {
|
|
if (targetOrganization.slug) {
|
|
return;
|
|
}
|
|
|
|
if (!orgMetadata?.requestedSlug) {
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: `Org with id: ${targetOrgId} doesn't have a slug. Tried using requestedSlug but that's also not present. So, all migration done but failed to set the Organization slug. Please set it manually`,
|
|
});
|
|
}
|
|
await setOrgSlug({
|
|
targetOrgId,
|
|
targetSlug: orgMetadata.requestedSlug,
|
|
});
|
|
}
|
|
|
|
function assertUserPartOfOrgAndRemigrationAllowed(
|
|
userToMoveToOrg: {
|
|
organizationId: number | null;
|
|
},
|
|
targetOrgId: number,
|
|
targetOrgUsername: string,
|
|
userId: number | undefined
|
|
) {
|
|
if (userToMoveToOrg.organizationId) {
|
|
if (userToMoveToOrg.organizationId !== targetOrgId) {
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: `User ${targetOrgUsername} already exists for different Org with orgId: ${targetOrgId}`,
|
|
});
|
|
} else {
|
|
log.debug(`Redoing migration for userId: ${userId} to orgId:${targetOrgId}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function getTeamOrThrowError(targetOrgId: number) {
|
|
const team = await prisma.team.findUnique({
|
|
where: {
|
|
id: targetOrgId,
|
|
},
|
|
include: {
|
|
organizationSettings: true,
|
|
},
|
|
});
|
|
|
|
if (!team) {
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: `Org with id: ${targetOrgId} not found`,
|
|
});
|
|
}
|
|
return team;
|
|
}
|
|
|
|
function assertUserPartOfOtherOrg(
|
|
userToMoveToOrg: {
|
|
organizationId: number | null;
|
|
} | null,
|
|
userName: string | undefined,
|
|
userId: number | undefined,
|
|
targetOrgId: number
|
|
): asserts userToMoveToOrg {
|
|
if (!userToMoveToOrg) {
|
|
throw new HttpError({
|
|
message: `User ${userName ? userName : `ID:${userId}`} is part of an org already`,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
if (userToMoveToOrg.organizationId && userToMoveToOrg.organizationId !== targetOrgId) {
|
|
throw new HttpError({
|
|
message: `User is already a part of different organization ID: ${userToMoveToOrg.organizationId}`,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
}
|
|
|
|
function assertUserIdOrUserName(userId: number | undefined, userName: string | undefined) {
|
|
if (!userId && !userName) {
|
|
throw new HttpError({ statusCode: 400, message: "userId or userName is required" });
|
|
}
|
|
if (userId && userName) {
|
|
throw new HttpError({ statusCode: 400, message: "Provide either userId or userName" });
|
|
}
|
|
}
|
|
|
|
async function addRedirect({
|
|
nonOrgUserName,
|
|
organization,
|
|
targetOrgUsername,
|
|
teamsToBeMovedToOrg,
|
|
}: {
|
|
nonOrgUserName: string | null;
|
|
organization: Team;
|
|
targetOrgUsername: string;
|
|
teamsToBeMovedToOrg: { slug: string | null }[];
|
|
}) {
|
|
if (!nonOrgUserName) {
|
|
return;
|
|
}
|
|
const orgSlug = organization.slug || (organization.metadata as { requestedSlug?: string })?.requestedSlug;
|
|
if (!orgSlug) {
|
|
log.debug("No slug for org. Not adding the redirect", safeStringify({ organization, nonOrgUserName }));
|
|
return;
|
|
}
|
|
// If the user had a username earlier, we need to redirect it to the new org username
|
|
const orgUrlPrefix = getOrgFullOrigin(orgSlug);
|
|
log.debug({
|
|
orgUrlPrefix,
|
|
nonOrgUserName,
|
|
targetOrgUsername,
|
|
});
|
|
|
|
await prisma.tempOrgRedirect.upsert({
|
|
where: {
|
|
from_type_fromOrgId: {
|
|
type: RedirectType.User,
|
|
from: nonOrgUserName,
|
|
fromOrgId: 0,
|
|
},
|
|
},
|
|
create: {
|
|
type: RedirectType.User,
|
|
from: nonOrgUserName,
|
|
fromOrgId: 0,
|
|
toUrl: `${orgUrlPrefix}/${targetOrgUsername}`,
|
|
},
|
|
update: {
|
|
toUrl: `${orgUrlPrefix}/${targetOrgUsername}`,
|
|
},
|
|
});
|
|
|
|
for (const [, team] of Object.entries(teamsToBeMovedToOrg)) {
|
|
if (!team.slug) {
|
|
log.debug("No slug for team. Not adding the redirect", safeStringify({ team }));
|
|
continue;
|
|
}
|
|
await prisma.tempOrgRedirect.upsert({
|
|
where: {
|
|
from_type_fromOrgId: {
|
|
type: RedirectType.Team,
|
|
from: team.slug,
|
|
fromOrgId: 0,
|
|
},
|
|
},
|
|
create: {
|
|
type: RedirectType.Team,
|
|
from: team.slug,
|
|
fromOrgId: 0,
|
|
toUrl: `${orgUrlPrefix}/team/${team.slug}`,
|
|
},
|
|
update: {
|
|
toUrl: `${orgUrlPrefix}/team/${team.slug}`,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
async function updateMembership({
|
|
targetOrgId,
|
|
userToMoveToOrg,
|
|
targetOrgRole,
|
|
targetOrgMembershipAccepted,
|
|
}: {
|
|
targetOrgId: number;
|
|
userToMoveToOrg: User;
|
|
targetOrgRole: MembershipRole;
|
|
targetOrgMembershipAccepted: boolean;
|
|
}) {
|
|
log.debug("updateMembership", { targetOrgId, userToMoveToOrg, targetOrgRole, targetOrgMembershipAccepted });
|
|
await prisma.membership.upsert({
|
|
where: {
|
|
userId_teamId: {
|
|
teamId: targetOrgId,
|
|
userId: userToMoveToOrg.id,
|
|
},
|
|
},
|
|
create: {
|
|
teamId: targetOrgId,
|
|
userId: userToMoveToOrg.id,
|
|
role: targetOrgRole,
|
|
accepted: targetOrgMembershipAccepted,
|
|
},
|
|
update: {
|
|
role: targetOrgRole,
|
|
accepted: targetOrgMembershipAccepted,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function dbMoveUserToOrg({
|
|
userToMoveToOrg,
|
|
targetOrgId,
|
|
targetOrgUsername,
|
|
nonOrgUserName,
|
|
}: {
|
|
userToMoveToOrg: User;
|
|
targetOrgId: number;
|
|
targetOrgUsername: string;
|
|
nonOrgUserName: string | null;
|
|
}) {
|
|
await prisma.user.update({
|
|
where: {
|
|
id: userToMoveToOrg.id,
|
|
},
|
|
data: {
|
|
organizationId: targetOrgId,
|
|
username: targetOrgUsername,
|
|
metadata: {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-expect-error
|
|
...(userToMoveToOrg.metadata || {}),
|
|
migratedToOrgFrom: {
|
|
username: nonOrgUserName,
|
|
lastMigrationTime: new Date().toISOString(),
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
await prisma.profile.upsert({
|
|
create: {
|
|
uid: ProfileRepository.generateProfileUid(),
|
|
userId: userToMoveToOrg.id,
|
|
organizationId: targetOrgId,
|
|
username: targetOrgUsername,
|
|
movedFromUser: {
|
|
connect: {
|
|
id: userToMoveToOrg.id,
|
|
},
|
|
},
|
|
},
|
|
update: {
|
|
organizationId: targetOrgId,
|
|
username: targetOrgUsername,
|
|
movedFromUser: {
|
|
connect: {
|
|
id: userToMoveToOrg.id,
|
|
},
|
|
},
|
|
},
|
|
where: {
|
|
userId_organizationId: {
|
|
userId: userToMoveToOrg.id,
|
|
organizationId: targetOrgId,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async function moveTeamsWithoutMembersToOrg({
|
|
targetOrgId,
|
|
userToMoveToOrg,
|
|
}: {
|
|
targetOrgId: number;
|
|
userToMoveToOrg: User;
|
|
}) {
|
|
const memberships = await prisma.membership.findMany({
|
|
where: {
|
|
userId: userToMoveToOrg.id,
|
|
},
|
|
});
|
|
|
|
const membershipTeamIds = memberships.map((m) => m.teamId);
|
|
const teams = await prisma.team.findMany({
|
|
where: {
|
|
id: {
|
|
in: membershipTeamIds,
|
|
},
|
|
},
|
|
select: {
|
|
id: true,
|
|
slug: true,
|
|
metadata: true,
|
|
isOrganization: true,
|
|
},
|
|
});
|
|
|
|
const teamsToBeMovedToOrg = teams
|
|
.map((team) => {
|
|
return {
|
|
...team,
|
|
metadata: teamMetadataSchema.parse(team.metadata),
|
|
};
|
|
})
|
|
// Remove Orgs from the list
|
|
.filter((team) => !team.isOrganization);
|
|
|
|
const teamIdsToBeMovedToOrg = teamsToBeMovedToOrg.map((t) => t.id);
|
|
|
|
if (memberships.length) {
|
|
// Add the user's teams to the org
|
|
await prisma.team.updateMany({
|
|
where: {
|
|
id: {
|
|
in: teamIdsToBeMovedToOrg,
|
|
},
|
|
},
|
|
data: {
|
|
parentId: targetOrgId,
|
|
},
|
|
});
|
|
}
|
|
return teamsToBeMovedToOrg;
|
|
}
|
|
|
|
/**
|
|
* Make sure you pass it an organization ID only and not a team ID.
|
|
*/
|
|
async function setOrgSlug({ targetOrgId, targetSlug }: { targetOrgId: number; targetSlug: string }) {
|
|
await prisma.team.update({
|
|
where: {
|
|
id: targetOrgId,
|
|
},
|
|
data: {
|
|
slug: targetSlug,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function removeUserAlongWithItsTeamsRedirects({
|
|
nonOrgUserName,
|
|
teamsToBeRemovedFromOrg,
|
|
}: {
|
|
nonOrgUserName: string | null;
|
|
teamsToBeRemovedFromOrg: { slug: string | null }[];
|
|
}) {
|
|
if (!nonOrgUserName) {
|
|
return;
|
|
}
|
|
|
|
await prisma.tempOrgRedirect.deleteMany({
|
|
// This where clause is unique, so we will get only one result but using deleteMany because it doesn't throw an error if there are no rows to delete
|
|
where: {
|
|
type: RedirectType.User,
|
|
from: nonOrgUserName,
|
|
fromOrgId: 0,
|
|
},
|
|
});
|
|
|
|
for (const [, team] of Object.entries(teamsToBeRemovedFromOrg)) {
|
|
if (!team.slug) {
|
|
log.debug("No slug for team. Not removing the redirect", safeStringify({ team }));
|
|
continue;
|
|
}
|
|
await prisma.tempOrgRedirect.deleteMany({
|
|
where: {
|
|
type: RedirectType.Team,
|
|
from: team.slug,
|
|
fromOrgId: 0,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
async function removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg }: { userToRemoveFromOrg: User }) {
|
|
const memberships = await prisma.membership.findMany({
|
|
where: {
|
|
userId: userToRemoveFromOrg.id,
|
|
},
|
|
});
|
|
|
|
const membershipTeamIds = memberships.map((m) => m.teamId);
|
|
const teams = await prisma.team.findMany({
|
|
where: {
|
|
id: {
|
|
in: membershipTeamIds,
|
|
},
|
|
},
|
|
select: {
|
|
id: true,
|
|
slug: true,
|
|
metadata: true,
|
|
isOrganization: true,
|
|
},
|
|
});
|
|
|
|
const teamsToBeRemovedFromOrg = teams
|
|
.map((team) => {
|
|
return {
|
|
...team,
|
|
metadata: teamMetadataSchema.parse(team.metadata),
|
|
};
|
|
})
|
|
// Remove Orgs from the list
|
|
.filter((team) => !team.isOrganization);
|
|
|
|
const teamIdsToBeRemovedFromOrg = teamsToBeRemovedFromOrg.map((t) => t.id);
|
|
|
|
if (memberships.length) {
|
|
// Remove the user's teams from the org
|
|
await prisma.team.updateMany({
|
|
where: {
|
|
id: {
|
|
in: teamIdsToBeRemovedFromOrg,
|
|
},
|
|
},
|
|
data: {
|
|
parentId: null,
|
|
},
|
|
});
|
|
}
|
|
return teamsToBeRemovedFromOrg;
|
|
}
|
|
|
|
async function dbRemoveUserFromOrg({
|
|
userToRemoveFromOrg,
|
|
nonOrgUserName,
|
|
}: {
|
|
userToRemoveFromOrg: User;
|
|
nonOrgUserName: string;
|
|
}) {
|
|
await prisma.user.update({
|
|
where: {
|
|
id: userToRemoveFromOrg.id,
|
|
},
|
|
data: {
|
|
organizationId: null,
|
|
username: nonOrgUserName,
|
|
metadata: {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-expect-error
|
|
...(userToRemoveFromOrg.metadata || {}),
|
|
migratedToOrgFrom: {
|
|
username: null,
|
|
reverted: true,
|
|
revertTime: new Date().toISOString(),
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
await ProfileRepository.deleteMany({
|
|
userIds: [userToRemoveFromOrg.id],
|
|
});
|
|
}
|
|
|
|
async function removeMembership({
|
|
targetOrgId,
|
|
userToRemoveFromOrg,
|
|
}: {
|
|
targetOrgId: number;
|
|
userToRemoveFromOrg: User;
|
|
}) {
|
|
await prisma.membership.deleteMany({
|
|
where: {
|
|
teamId: targetOrgId,
|
|
userId: userToRemoveFromOrg.id,
|
|
},
|
|
});
|
|
}
|