* Add profile table Copy to profile table Update schema Migrations * wip * Get switcher working * Add organization seeder * wip * Remove dead code * Fix unit tests * relevantProfile -> profile * cache bust * One more comment * Squash migrations * Copy to profile table * Update schema * Fix existing user invite flow * Add profile table * Copy to profile table * Update schema * Format * Add profile table Copy to profile table Update schema Migrations * Format * wip Get switcher working Add organization seeder wip Remove dead code Fix unit tests relevantProfile -> profile cache bust One more comment Squash migrations Copy to profile table Update schema Fix existing user invite flow Add profile table Copy to profile table Update schema chore: small improvements to signup (#13227) * removed features on mobile, better background color * nit * dark mode nits New Crowdin translations by Github Action fix: event reschedule workflow trigger (#13122) * fix event reschedule workflow trigger * uncomment previously failed test * review fix --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> chore: added community support to help (#13213) refactor: Abstract `handleSeats` from `handleNewBooking` (#12062) * Refactor createBooking * Type fix * Abstract handleSeats * Create Invitee type * Create OrganizerUser type * Abstract addVideoCallDataToEvt * Abstract createLoggerWithEventDetails * Abstract `handleAppStatus` from handler * Create ReqAppsStatus type * Move `deleteMeeting` and `getCalendar` * Set parameters for `handleSeats` * Typescript refactor * Change function params from req * Type fix --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Erik <erik@erosemberg.com> fix: Dynamic group booking org-migration redirect (#13223) * fix: Dynamic group booking redirect * Fix flaky private team test test: Check availability tab and all your functions (teste2e-availability) (#12434) * add changes * add changes * add changes * add changes * Requested changes * Change way to grab element --------- Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com> Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> fix: Reschedule a cancelled booking (#13170) * fix: Reschedule a cancelled booking * canceled instead of cancelled --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> v3.7.0 test: In Team Event type when location is Organizer Default App and U… (#12939) * test: In Team Event type when location is Organizer Default App and User Metadata is emoty * chore: use variable * fix: type error test: add test for invalid cal video (#13103) * test: add test for invalid cal video * chore: add confirm * chore: fix type error * chore: update code * test: add unit test * chore: type error * chore * chore: type * chore: fix test * chore --------- Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> chore: add error message for no availability (#13230) * chore: add error message for no default user availability * chore: check only availability * chore: change message * chore: add eventType fix: Team revert with members (#13228) refactor: Refactor seats logic (#12905) * Refactor createBooking * Type fix * Abstract handleSeats * Create Invitee type * Create OrganizerUser type * Abstract addVideoCallDataToEvt * Abstract createLoggerWithEventDetails * Abstract `handleAppStatus` from handler * Create ReqAppsStatus type * Move `deleteMeeting` and `getCalendar` * Set parameters for `handleSeats` * Typescript refactor * Change function params from req * Type fix * Move handleSeats * Abstract lastAttendeeDeleteBooking * Create function for rescheduling seated events * Fix imports on reschedule seats function * Fix imports * Import handleSeats function * Fix rescheduleUid type * Refactor owner reschedule to new time slot * Refactor combine two booking times together * Reschedule as an attendee * Refactor createNewSeat * Remove old handleSeats * Remove lastAttendeeDeleteBooking from handleNewBooking * Test for new attendee right params are passed * Unit test params for reschedule * Typo fix * Clean up * Create new seat test * Test when attendee already signs up for booking * Type fix * Test reschedule move attendee to existing booking * On reschedule create new booking * Test on last attendee cancel booking * Owner reschedule to new time slot * Owner rescheduling, merge two bookings together * Test: when merging more than available seats, then fail * Test: fail when event is full * Remove duplicate E2E tests * Clean up * Rename `addVideoCallDataToEvt` to `addVideoCallDataToEvent` * Refactor `calcAppsStatus` * Assign `evt` to resutl of `addVideoCallDataToEvent` * Use prisma.transaction when moving attendees * Clean create seat call * Use ErrorCode enum * Use attendeeRescheduledSeatedBooking function * Await function * Prevent double triggering of workflows * Use inviteeToAdd in createNewSeat * Remove unused error code * Remove old handleSeats file * Type fix * Type fix * Type fix * Type fix * Type fix * Type fix * Type fix * Type fix --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Erik <erik@erosemberg.com> feat: Allow admins to filter bookings only by attendee emails (#13236) * chore: Allow admins to filter bookings only by attendee emails * Fix type error Fixed [CAL-2930] Org branding isnt centered in shell #13225 (#13237) Add profile table Copy to profile table Update schema Migrations Format * wip * Add profile relation everywhere * Fix schema * Fix invite handler * Add profileId during eventTypec reation * Get dashboard working with profile relatioon * Add workflow create repository * Add profileId to host * Add profileId to credential table * Add destinationCalendar repository * Add profileId to membership * Revert all profile related changes * Add acme and dunder * Fix membership profileId usgae * Various fixes * Some renaming * Fix failing tests * Debugging next-aut * Consolidate migrations * Testing fixes * Fix unit tests * Self review feedback * PR feedback * Fix orgMigration as well * Add profile schema and copy users to it * Remove migrations from here * Fix type issues * Fix type error after main merge * Remove movedFromUserId * Add missing migration file * Self review addressed * Joe's review addressed * Fix profile switcher --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
894 lines
22 KiB
TypeScript
894 lines
22 KiB
TypeScript
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
|
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
|
import { HttpError } from "@calcom/lib/http-error";
|
|
import logger from "@calcom/lib/logger";
|
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
|
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
|
import prisma from "@calcom/prisma";
|
|
import type { Team, User } from "@calcom/prisma/client";
|
|
import { RedirectType } from "@calcom/prisma/client";
|
|
import { Prisma } from "@calcom/prisma/client";
|
|
import type { MembershipRole } from "@calcom/prisma/enums";
|
|
import { teamMetadataSchema } 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: 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 (!teamMetadata?.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.metadata.orgAutoAcceptEmail || ""
|
|
);
|
|
}
|
|
|
|
const userWithSameUsernameInOrg = await prisma.user.findFirst({
|
|
where: {
|
|
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}`);
|
|
}
|
|
|
|
/**
|
|
* 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 (!teamMetadata?.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,
|
|
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;
|
|
},
|
|
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,
|
|
},
|
|
});
|
|
|
|
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 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,
|
|
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-ignore
|
|
...(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,
|
|
},
|
|
});
|
|
|
|
const teamsToBeMovedToOrg = teams
|
|
.map((team) => {
|
|
return {
|
|
...team,
|
|
metadata: teamMetadataSchema.parse(team.metadata),
|
|
};
|
|
})
|
|
// Remove Orgs from the list
|
|
.filter((team) => !team.metadata?.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 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,
|
|
}: {
|
|
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 dbRemoveTeamFromOrg({ teamId }: { teamId: number }) {
|
|
const team = await prisma.team.findUnique({
|
|
where: {
|
|
id: teamId,
|
|
},
|
|
});
|
|
|
|
if (!team) {
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: `Team with id: ${teamId} not found`,
|
|
});
|
|
}
|
|
|
|
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
|
|
try {
|
|
return await prisma.team.update({
|
|
where: {
|
|
id: teamId,
|
|
},
|
|
data: {
|
|
parentId: null,
|
|
slug: teamMetadata?.migratedToOrgFrom?.teamSlug || team.slug,
|
|
metadata: {
|
|
...teamMetadata,
|
|
migratedToOrgFrom: {
|
|
reverted: true,
|
|
lastRevertTime: new Date().toISOString(),
|
|
},
|
|
},
|
|
},
|
|
include: {
|
|
members: true,
|
|
},
|
|
});
|
|
} catch (e) {
|
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
if (e.code === "P2002") {
|
|
throw new HttpError({
|
|
message: `Looks like the team's name is already taken by some other team outside the org or an org itself. Please change this team's name or the other team/org's name. If you rename the team that you are trying to remove from the org, you will have to manually remove the redirect from the database for that team as the slug would have changed.`,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
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,
|
|
},
|
|
});
|
|
|
|
const teamsToBeRemovedFromOrg = teams
|
|
.map((team) => {
|
|
return {
|
|
...team,
|
|
metadata: teamMetadataSchema.parse(team.metadata),
|
|
};
|
|
})
|
|
// Remove Orgs from the list
|
|
.filter((team) => !team.metadata?.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-ignore
|
|
...(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,
|
|
},
|
|
});
|
|
}
|