fix: Consistently remove a member from organization/sub-team/team - through all the APIs and webapp actions (#22806)
* Fix membership deletion handling and add tests * fixes * revert api-v2 specific changes * Add more tests * refactor: Move removeMember function into TeamService as private static methods - Moved removeMember and all related helper functions from separate file into TeamService class - Made all functions private static methods instead of exporting them - Deleted the original removeMember.ts file since it's no longer needed - Updated imports to use the new location - All integration tests pass successfully * refactor: Rename memberId to userId in TeamService methods - Renamed memberId parameter to userId in removeMember private method - Renamed memberIds parameter to userIds in removeMembers public method - Updated all calls to removeMembers to use userIds instead of memberIds - Parameter names now accurately reflect that they are user IDs, not membership IDs * test: add service unit tests and simplify e2e tests for membership deletion - Add unit tests for membership deletion services - Remove redundant deletion behavior tests from controllers - Keep only happy path tests in e2e controller tests - Fix unused imports and variables * package.json version * fix unit teswtes * No specs file * fix unit tests * fix: Add platform-libraries build step to E2E API v2 workflow The E2E API v2 tests were failing with TypeScript compilation errors because TeamService imports from @calcom/platform-libraries require the package to be built first to generate the dist/ folder with compiled exports. This adds the same build step that was added to unit-tests.yml to resolve the module resolution errors. Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Making tests clearer and easier to undestand * chore: bump platform libs * chore: bump platform libs --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: cal.com <morgan@cal.com>
This commit is contained in:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Morgan
cal.com
parent
f74434a282
commit
88bbab216e
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.312",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.314",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
+17
-1
@@ -2,6 +2,8 @@ import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inpu
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
||||
|
||||
@@ -62,10 +64,24 @@ export class OrganizationsMembershipService {
|
||||
}
|
||||
|
||||
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
||||
const membership = await this.organizationsMembershipRepository.deleteOrgMembership(
|
||||
// Get the membership first to get the userId
|
||||
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
||||
organizationId,
|
||||
membershipId
|
||||
);
|
||||
|
||||
if (!membership) {
|
||||
throw new NotFoundException(
|
||||
`Membership with id ${membershipId} within organization id ${organizationId} not found`
|
||||
);
|
||||
}
|
||||
|
||||
await TeamService.removeMembers({
|
||||
teamIds: [organizationId],
|
||||
userIds: [membership.userId],
|
||||
isOrg: true,
|
||||
});
|
||||
|
||||
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||
}
|
||||
|
||||
|
||||
-4
@@ -20,7 +20,6 @@ import {
|
||||
OrgTeamMembershipOutputResponseDto,
|
||||
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
|
||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
||||
import {
|
||||
Controller,
|
||||
@@ -59,7 +58,6 @@ export class OrganizationsTeamsMembershipsController {
|
||||
|
||||
constructor(
|
||||
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
|
||||
private teamsEventTypesService: TeamsEventTypesService,
|
||||
private readonly organizationsRepository: OrganizationsRepository
|
||||
) {}
|
||||
|
||||
@@ -127,8 +125,6 @@ export class OrganizationsTeamsMembershipsController {
|
||||
membershipId
|
||||
);
|
||||
|
||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||
|
||||
+13
-1
@@ -3,6 +3,8 @@ import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/member
|
||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsTeamsMembershipsService {
|
||||
constructor(
|
||||
@@ -58,11 +60,21 @@ export class OrganizationsTeamsMembershipsService {
|
||||
}
|
||||
|
||||
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
|
||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.deleteOrgTeamMembershipById(
|
||||
// First get the membership to get the userId
|
||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.findOrgTeamMembership(
|
||||
organizationId,
|
||||
teamId,
|
||||
membershipId
|
||||
);
|
||||
|
||||
if (!teamMembership) {
|
||||
throw new NotFoundException(
|
||||
`Membership with id ${membershipId} not found in team ${teamId} of organization ${organizationId}`
|
||||
);
|
||||
}
|
||||
|
||||
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||
|
||||
return teamMembership;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,17 +156,4 @@ export class TeamsEventTypesService {
|
||||
|
||||
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
||||
}
|
||||
|
||||
async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) {
|
||||
try {
|
||||
await this.teamsEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId);
|
||||
await this.teamsEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId);
|
||||
} catch (err) {
|
||||
this.logger.error("Could not remove user from all team event-types.", {
|
||||
error: err,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-8
@@ -3,7 +3,6 @@ import { API_KEY_HEADER } from "@/lib/docs/headers";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
||||
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
||||
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
||||
@@ -45,10 +44,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
||||
export class TeamsMembershipsController {
|
||||
private logger = new Logger("TeamsMembershipsController");
|
||||
|
||||
constructor(
|
||||
private teamsMembershipsService: TeamsMembershipsService,
|
||||
private teamsEventTypesService: TeamsEventTypesService
|
||||
) {}
|
||||
constructor(private teamsMembershipsService: TeamsMembershipsService) {}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@Post("/")
|
||||
@@ -127,7 +123,6 @@ export class TeamsMembershipsController {
|
||||
membershipId,
|
||||
body
|
||||
);
|
||||
|
||||
if (!currentMembership.accepted && updatedMembership.accepted) {
|
||||
try {
|
||||
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
||||
@@ -151,8 +146,6 @@ export class TeamsMembershipsController {
|
||||
): Promise<DeleteTeamMembershipOutput> {
|
||||
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
|
||||
|
||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||
|
||||
@@ -3,6 +3,8 @@ import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/up
|
||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class TeamsMembershipsService {
|
||||
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
||||
@@ -41,10 +43,15 @@ export class TeamsMembershipsService {
|
||||
}
|
||||
|
||||
async deleteTeamMembership(teamId: number, membershipId: number) {
|
||||
const teamMembership = await this.teamsMembershipsRepository.deleteTeamMembershipById(
|
||||
teamId,
|
||||
membershipId
|
||||
);
|
||||
// First get the membership to get the userId
|
||||
const teamMembership = await this.teamsMembershipsRepository.findTeamMembership(teamId, membershipId);
|
||||
|
||||
if (!teamMembership) {
|
||||
throw new NotFoundException(`Membership with id ${membershipId} not found in team ${teamId}`);
|
||||
}
|
||||
|
||||
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||
|
||||
return teamMembership;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,4 +59,8 @@ export class EventTypesRepositoryFixture {
|
||||
async delete(eventTypeId: EventType["id"]) {
|
||||
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
|
||||
}
|
||||
|
||||
async findById(eventTypeId: EventType["id"]) {
|
||||
return this.prismaReadClient.eventType.findUnique({ where: { id: eventTypeId } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,4 +35,8 @@ export class MembershipRepositoryFixture {
|
||||
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
||||
return membership;
|
||||
}
|
||||
|
||||
async findById(membershipId: Membership["id"]) {
|
||||
return this.prismaReadClient.membership.findUnique({ where: { id: membershipId } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
|
||||
@@ -55,7 +55,7 @@ export const EventMeta = ({
|
||||
timeZones,
|
||||
children,
|
||||
selectedTimeslot,
|
||||
roundRobinHideOrgAndTeam
|
||||
roundRobinHideOrgAndTeam,
|
||||
}: {
|
||||
event?: Pick<
|
||||
BookerEvent,
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import removeMember from "@calcom/features/ee/teams/lib/removeMember";
|
||||
import { TeamService } from "@calcom/lib/server/service/teamService";
|
||||
|
||||
const removeUserFromOrg = async ({ userId, orgId }: { userId: number; orgId: number }) => {
|
||||
return removeMember({
|
||||
memberId: userId,
|
||||
teamId: orgId,
|
||||
isOrg: true,
|
||||
});
|
||||
return TeamService.removeMembers({ teamIds: [orgId], userIds: [userId], isOrg: true });
|
||||
};
|
||||
|
||||
export default removeUserFromOrg;
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { deleteWorkfowRemindersOfRemovedMember } from "./deleteWorkflowRemindersOfRemovedMember";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["removeMember"] });
|
||||
|
||||
const removeMember = async ({
|
||||
memberId,
|
||||
teamId,
|
||||
isOrg,
|
||||
}: {
|
||||
memberId: number;
|
||||
teamId: number;
|
||||
isOrg: boolean;
|
||||
}) => {
|
||||
const [membership] = await prisma.$transaction([
|
||||
prisma.membership.delete({
|
||||
where: {
|
||||
userId_teamId: { userId: memberId, teamId: teamId },
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
team: true,
|
||||
},
|
||||
}),
|
||||
// remove user as host from team events associated with this membership
|
||||
prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: memberId,
|
||||
eventType: {
|
||||
teamId: teamId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const team = await prisma.team.findUnique({
|
||||
where: { id: teamId },
|
||||
select: {
|
||||
isOrganization: true,
|
||||
organizationSettings: true,
|
||||
id: true,
|
||||
metadata: true,
|
||||
activeOrgWorkflows: true,
|
||||
parentId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const foundUser = await prisma.user.findUnique({
|
||||
where: { id: memberId },
|
||||
select: {
|
||||
id: true,
|
||||
movedToProfileId: true,
|
||||
email: true,
|
||||
username: true,
|
||||
completedOnboarding: true,
|
||||
teams: {
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!team || !foundUser) throw new TRPCError({ code: "NOT_FOUND" });
|
||||
|
||||
if (isOrg) {
|
||||
log.debug("Removing a member from the organization");
|
||||
// Deleting membership from all child teams
|
||||
// Delete all sub-team memberships where this team is the organization
|
||||
await prisma.membership.deleteMany({
|
||||
where: {
|
||||
team: {
|
||||
parentId: teamId,
|
||||
},
|
||||
userId: membership.userId,
|
||||
},
|
||||
});
|
||||
|
||||
const userToDeleteMembershipOf = foundUser;
|
||||
|
||||
const profileToDelete = await ProfileRepository.findByUserIdAndOrgId({
|
||||
userId: userToDeleteMembershipOf.id,
|
||||
organizationId: team.id,
|
||||
});
|
||||
|
||||
if (
|
||||
userToDeleteMembershipOf.username &&
|
||||
userToDeleteMembershipOf.movedToProfileId === profileToDelete?.id
|
||||
) {
|
||||
log.debug("Cleaning up tempOrgRedirect for user", userToDeleteMembershipOf.username);
|
||||
|
||||
await prisma.tempOrgRedirect.deleteMany({
|
||||
where: {
|
||||
from: userToDeleteMembershipOf.username,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.user.update({
|
||||
where: { id: membership.userId },
|
||||
data: { organizationId: null },
|
||||
}),
|
||||
ProfileRepository.delete({
|
||||
userId: membership.userId,
|
||||
organizationId: team.id,
|
||||
}),
|
||||
prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: memberId,
|
||||
eventType: {
|
||||
team: {
|
||||
parentId: teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
// Deleted managed event types from this team from this member
|
||||
await prisma.eventType.deleteMany({
|
||||
where: { parent: { teamId: teamId }, userId: membership.userId },
|
||||
});
|
||||
|
||||
await deleteWorkfowRemindersOfRemovedMember(team, memberId, isOrg);
|
||||
|
||||
return { membership };
|
||||
};
|
||||
|
||||
export default removeMember;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,52 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { TeamBilling } from "@calcom/features/ee/billing/teams";
|
||||
import removeMember from "@calcom/features/ee/teams/lib/removeMember";
|
||||
import { deleteWorkfowRemindersOfRemovedMember } from "@calcom/features/ee/teams/lib/deleteWorkflowRemindersOfRemovedMember";
|
||||
import { deleteDomain } from "@calcom/lib/domainManager/organization";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||
import { WorkflowService } from "@calcom/lib/server/service/workflows";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Membership } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["TeamService"] });
|
||||
|
||||
type MembershipWithRelations = Pick<
|
||||
Membership,
|
||||
"id" | "userId" | "teamId" | "role" | "accepted" | "disableImpersonation"
|
||||
>;
|
||||
|
||||
type TeamWithSettings = {
|
||||
id: number;
|
||||
isOrganization: boolean | null;
|
||||
organizationSettings: unknown;
|
||||
metadata: unknown;
|
||||
activeOrgWorkflows: unknown;
|
||||
parentId: number | null;
|
||||
};
|
||||
|
||||
type UserWithTeams = {
|
||||
id: number;
|
||||
movedToProfileId: number | null;
|
||||
email: string;
|
||||
username: string | null;
|
||||
completedOnboarding: boolean;
|
||||
teams: {
|
||||
team: {
|
||||
id: number;
|
||||
parentId: number | null;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
export type RemoveMemberResult = {
|
||||
membership: MembershipWithRelations;
|
||||
};
|
||||
|
||||
export class TeamService {
|
||||
/**
|
||||
* Deletes a team and all its associated data in a safe, transactional order.
|
||||
@@ -45,15 +81,23 @@ export class TeamService {
|
||||
return deletedTeam;
|
||||
}
|
||||
|
||||
static async removeMembers(teamIds: number[], memberIds: number[], isOrg = false) {
|
||||
const deleteMembershipPromises = [];
|
||||
static async removeMembers({
|
||||
teamIds,
|
||||
userIds,
|
||||
isOrg = false,
|
||||
}: {
|
||||
teamIds: number[];
|
||||
userIds: number[];
|
||||
isOrg?: boolean;
|
||||
}) {
|
||||
const deleteMembershipPromises: Promise<RemoveMemberResult>[] = [];
|
||||
|
||||
for (const memberId of memberIds) {
|
||||
for (const userId of userIds) {
|
||||
for (const teamId of teamIds) {
|
||||
deleteMembershipPromises.push(
|
||||
removeMember({
|
||||
TeamService.removeMember({
|
||||
teamId,
|
||||
memberId,
|
||||
userId,
|
||||
isOrg,
|
||||
})
|
||||
);
|
||||
@@ -121,4 +165,217 @@ export class TeamService {
|
||||
const teamBilling = await TeamBilling.findAndInit(teamId);
|
||||
return teamBilling.publish();
|
||||
}
|
||||
|
||||
private static async removeMember({
|
||||
userId,
|
||||
teamId,
|
||||
isOrg,
|
||||
}: {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
isOrg: boolean;
|
||||
}) {
|
||||
const membership = await TeamService.fetchMembershipOrThrow(userId, teamId);
|
||||
const team = await TeamService.fetchTeamOrThrow(teamId);
|
||||
const user = await TeamService.fetchUserOrThrow(userId);
|
||||
|
||||
if (isOrg) {
|
||||
log.debug("Removing a member from the organization");
|
||||
await TeamService.removeFromOrganization(membership, team, user);
|
||||
} else {
|
||||
log.debug("Removing a member from a team");
|
||||
await TeamService.removeFromTeam(membership, teamId);
|
||||
}
|
||||
|
||||
await deleteWorkfowRemindersOfRemovedMember(team, userId, isOrg);
|
||||
|
||||
return { membership };
|
||||
}
|
||||
|
||||
// TODO: Needs to be moved to repository
|
||||
private static async fetchMembershipOrThrow(
|
||||
userId: number,
|
||||
teamId: number
|
||||
): Promise<MembershipWithRelations> {
|
||||
const membership = await prisma.membership.findUnique({
|
||||
where: {
|
||||
userId_teamId: { userId: userId, teamId: teamId },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
role: true,
|
||||
accepted: true,
|
||||
disableImpersonation: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Membership not found" });
|
||||
}
|
||||
|
||||
return membership;
|
||||
}
|
||||
|
||||
// TODO: Needs to be moved to repository
|
||||
private static async fetchTeamOrThrow(teamId: number): Promise<TeamWithSettings> {
|
||||
const team = await prisma.team.findUnique({
|
||||
where: { id: teamId },
|
||||
select: {
|
||||
isOrganization: true,
|
||||
organizationSettings: true,
|
||||
id: true,
|
||||
metadata: true,
|
||||
activeOrgWorkflows: true,
|
||||
parentId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Team not found" });
|
||||
}
|
||||
|
||||
return team;
|
||||
}
|
||||
|
||||
// TODO: Needs to be moved to repository
|
||||
private static async fetchUserOrThrow(userId: number): Promise<UserWithTeams> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
movedToProfileId: true,
|
||||
email: true,
|
||||
username: true,
|
||||
completedOnboarding: true,
|
||||
teams: {
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// TODO: Needs to be moved to repository
|
||||
private static async cleanupTempOrgRedirect(user: UserWithTeams, team: TeamWithSettings) {
|
||||
const profileToDelete = await ProfileRepository.findByUserIdAndOrgId({
|
||||
userId: user.id,
|
||||
organizationId: team.id,
|
||||
});
|
||||
|
||||
if (user.username && user.movedToProfileId === profileToDelete?.id) {
|
||||
log.debug("Cleaning up tempOrgRedirect for user", user.username);
|
||||
await prisma.tempOrgRedirect.deleteMany({
|
||||
where: {
|
||||
from: user.username,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static async removeFromOrganization(
|
||||
membership: MembershipWithRelations,
|
||||
team: TeamWithSettings,
|
||||
user: UserWithTeams
|
||||
) {
|
||||
await TeamService.cleanupTempOrgRedirect(user, team);
|
||||
const newUsername = generateNewUsername(user);
|
||||
|
||||
await prisma.$transaction([
|
||||
// Remove user from all sub-teams event type hosts
|
||||
prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: membership.userId,
|
||||
eventType: {
|
||||
team: {
|
||||
parentId: team.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Delete managed child events in sub-teams
|
||||
prisma.eventType.deleteMany({
|
||||
where: {
|
||||
userId: membership.userId,
|
||||
parent: {
|
||||
team: {
|
||||
parentId: team.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Remove organizationId from the user
|
||||
prisma.user.update({
|
||||
where: { id: membership.userId },
|
||||
data: {
|
||||
organizationId: null,
|
||||
username: newUsername,
|
||||
},
|
||||
}),
|
||||
// Delete the profile of the user from the organization
|
||||
ProfileRepository.delete({
|
||||
userId: membership.userId,
|
||||
organizationId: team.id,
|
||||
}),
|
||||
// Delete all sub-team memberships where this team is the organization
|
||||
prisma.membership.deleteMany({
|
||||
where: {
|
||||
team: {
|
||||
parentId: team.id,
|
||||
},
|
||||
userId: membership.userId,
|
||||
},
|
||||
}),
|
||||
// Delete the membership of the user from the organization
|
||||
prisma.membership.delete({
|
||||
where: {
|
||||
userId_teamId: { userId: membership.userId, teamId: team.id },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// Generate new username for user leaving organization
|
||||
function generateNewUsername(user: UserWithTeams): string | null {
|
||||
// We ensure that new username would be unique across all users in the global namespace outside any organization
|
||||
return user.username != null ? `${user.username}-${user.id}` : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove member from regular team
|
||||
private static async removeFromTeam(membership: MembershipWithRelations, teamId: number) {
|
||||
await prisma.$transaction([
|
||||
// Remove user from all team event types' hosts
|
||||
prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: membership.userId,
|
||||
eventType: {
|
||||
teamId: teamId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Deleted managed event types from this team for this member
|
||||
prisma.eventType.deleteMany({
|
||||
where: { parent: { teamId: teamId }, userId: membership.userId },
|
||||
}),
|
||||
// Delete the membership of the user from the team
|
||||
prisma.membership.delete({
|
||||
where: {
|
||||
userId_teamId: { userId: membership.userId, teamId: teamId },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,4 +120,5 @@ export { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
||||
|
||||
export { verifyPhoneNumber, sendVerificationCode };
|
||||
|
||||
export { TeamService } from "@calcom/lib/server/service/teamService";
|
||||
export { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
|
||||
|
||||
@@ -59,7 +59,7 @@ export const removeMemberHandler = async ({ ctx, input }: RemoveMemberOptions) =
|
||||
message: "You can not remove yourself from a team you own.",
|
||||
});
|
||||
|
||||
await TeamService.removeMembers(teamIds, memberIds, isOrg);
|
||||
await TeamService.removeMembers({ teamIds, userIds: memberIds, isOrg });
|
||||
};
|
||||
|
||||
export default removeMemberHandler;
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
// TODO: Bring this test back with the correct setup (no illegal imports)
|
||||
import prismaMock from "../../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import { describe, test, expect } from "vitest";
|
||||
|
||||
import { SchedulingType, MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../types";
|
||||
import removeMember from "./removeMember.handler";
|
||||
|
||||
describe.skip("removeMember", () => {
|
||||
describe("should remove a member from a team", () => {
|
||||
test(`1) Should remove a member from a team
|
||||
2) Should remove the member from hosts
|
||||
`, async () => {
|
||||
const org = await createOrganization({
|
||||
name: "Test Org",
|
||||
slug: "testorg",
|
||||
});
|
||||
|
||||
// Create the child team
|
||||
const childTeam = {
|
||||
id: 202,
|
||||
name: "Team 1",
|
||||
slug: "team-1",
|
||||
parentId: org.id,
|
||||
};
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
organizationId: org.id,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
teams: [
|
||||
{
|
||||
membership: {
|
||||
accepted: true,
|
||||
role: MembershipRole.ADMIN,
|
||||
},
|
||||
team: {
|
||||
id: org.id,
|
||||
name: "Test Org",
|
||||
slug: "testorg",
|
||||
},
|
||||
},
|
||||
{
|
||||
membership: {
|
||||
accepted: true,
|
||||
role: MembershipRole.ADMIN,
|
||||
},
|
||||
team: {
|
||||
id: childTeam.id,
|
||||
name: "Team 1",
|
||||
slug: "team-1",
|
||||
parentId: org.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const otherTeamMembers = [
|
||||
{
|
||||
name: "Other Team Member 1",
|
||||
username: "other-team-member-1",
|
||||
timeZone: Timezones["+5:30"],
|
||||
defaultScheduleId: null,
|
||||
email: "other-team-member-1@example.com",
|
||||
id: 102,
|
||||
organizationId: org.id,
|
||||
schedules: [TestData.schedules.IstEveningShift],
|
||||
teams: [
|
||||
{
|
||||
membership: {
|
||||
accepted: true,
|
||||
role: MembershipRole.MEMBER,
|
||||
},
|
||||
team: {
|
||||
id: org.id,
|
||||
name: "Test Org",
|
||||
slug: "testorg",
|
||||
},
|
||||
},
|
||||
{
|
||||
membership: {
|
||||
accepted: true,
|
||||
role: MembershipRole.MEMBER,
|
||||
},
|
||||
team: {
|
||||
id: childTeam.id,
|
||||
name: "Team 1",
|
||||
slug: "team-1",
|
||||
parentId: org.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData(
|
||||
{
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
teamId: childTeam.id,
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
length: 30,
|
||||
hosts: [
|
||||
{
|
||||
userId: 101,
|
||||
isFixed: false,
|
||||
},
|
||||
{
|
||||
userId: 102,
|
||||
isFixed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
usersApartFromOrganizer: otherTeamMembers,
|
||||
apps: [TestData.apps["daily-video"]],
|
||||
},
|
||||
org
|
||||
)
|
||||
);
|
||||
|
||||
//Logged in user is admin of the org
|
||||
const ctx = {
|
||||
user: {
|
||||
id: organizer.id,
|
||||
name: organizer.name,
|
||||
} as NonNullable<TrpcSessionUser>,
|
||||
};
|
||||
|
||||
await removeMember({
|
||||
ctx,
|
||||
input: {
|
||||
teamIds: [org.id],
|
||||
memberIds: [102],
|
||||
isOrg: true,
|
||||
},
|
||||
});
|
||||
|
||||
//Check if the remaining memberships are correct
|
||||
const remainingMemberships = await prismaMock.membership.count({
|
||||
where: {
|
||||
teamId: childTeam.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(remainingMemberships).toBe(1);
|
||||
|
||||
//Check if the event type has the correct hosts
|
||||
const remainingHostsCount = await prismaMock.host.count({
|
||||
where: {
|
||||
eventTypeId: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(remainingHostsCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2500,7 +2500,7 @@ __metadata:
|
||||
"@axiomhq/winston": ^1.2.0
|
||||
"@calcom/platform-constants": "*"
|
||||
"@calcom/platform-enums": "*"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.312"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.314"
|
||||
"@calcom/platform-types": "*"
|
||||
"@calcom/platform-utils": "*"
|
||||
"@calcom/prisma": "*"
|
||||
@@ -3559,13 +3559,13 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.312":
|
||||
version: 0.0.312
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.312"
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.314":
|
||||
version: 0.0.314
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.314"
|
||||
dependencies:
|
||||
"@calcom/features": "*"
|
||||
"@calcom/lib": "*"
|
||||
checksum: 46520737404a61f3aa96c6f1adfa81e67f645fd984f495662ca7c63184b0310117a1dad57ddbe7ea487f2e5a0c9cbe62749a9a43b3197a021b19e3ccadc1f5b2
|
||||
checksum: b59eb591273334ed7d7d07a8f0518eb8cb40255883dff49626f85d3bd804db4da357bf77cd4f7b14ae0743d0a699647232b39956af9123bdc5e7b2458cab30eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user