Skip to content

Commit 19d7764

Browse files
authored
refactor: platform managed user org admin access (calcom#22597)
* refactor: examples app setup * wip: view org event types as org administrator * fix: create event type has org admin * fix: create event type has org admin * fix: allow org admin team event type access * fix: uncaught error * chore: bump platform libs * refactor: getting event type for org admin * add test * chore: libraries
1 parent 30061c0 commit 19d7764

15 files changed

Lines changed: 857 additions & 228 deletions

File tree

apps/api/v2/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@axiomhq/winston": "^1.2.0",
3939
"@calcom/platform-constants": "*",
4040
"@calcom/platform-enums": "*",
41-
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.267",
41+
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.268",
4242
"@calcom/platform-types": "*",
4343
"@calcom/platform-utils": "*",
4444
"@calcom/prisma": "*",

apps/api/v2/src/modules/atoms/atoms.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { SchedulesAtomsService } from "@/modules/atoms/services/schedules-atom.s
1111
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
1212
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
1313
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
14+
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
1415
import { PrismaModule } from "@/modules/prisma/prisma.module";
1516
import { RedisService } from "@/modules/redis/redis.service";
1617
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
@@ -21,6 +22,7 @@ import { Module } from "@nestjs/common";
2122
@Module({
2223
imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule, TeamsEventTypesModule],
2324
providers: [
25+
OrganizationsTeamsRepository,
2426
EventTypesAtomService,
2527
ConferencingAtomsService,
2628
AttributesAtomsService,

apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { systemBeforeFieldEmail } from "@/ee/event-types/event-types_2024_06_14/
33
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
44
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
55
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
6+
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
67
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
78
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
89
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
@@ -53,7 +54,8 @@ export class EventTypesAtomService {
5354
private readonly dbWrite: PrismaWriteService,
5455
private readonly dbRead: PrismaReadService,
5556
private readonly eventTypeService: EventTypesService_2024_06_14,
56-
private readonly teamEventTypeService: TeamsEventTypesService
57+
private readonly teamEventTypeService: TeamsEventTypesService,
58+
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository
5759
) {}
5860

5961
private async getTeamSlug(teamId: number): Promise<string> {
@@ -88,10 +90,12 @@ export class EventTypesAtomService {
8890
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
8991
}
9092

91-
if (eventType?.team?.id) {
92-
await this.checkTeamOwnsEventType(user.id, eventType.eventType.id, eventType.team.id);
93-
} else {
94-
this.eventTypeService.checkUserOwnsEventType(user.id, eventType.eventType);
93+
if (!isUserOrganizationAdmin) {
94+
if (eventType?.team?.id) {
95+
await this.checkTeamOwnsEventType(user.id, eventType.eventType.id, eventType.team.id);
96+
} else {
97+
this.eventTypeService.checkUserOwnsEventType(user.id, eventType.eventType);
98+
}
9599
}
96100

97101
// note (Lauris): don't show platform owner as one of the people that can be assigned to managed team event type
@@ -115,7 +119,8 @@ export class EventTypesAtomService {
115119
user: UserWithProfile,
116120
teamId: number
117121
) {
118-
await this.checkCanUpdateTeamEventType(user.id, eventTypeId, teamId, body.scheduleId);
122+
await this.checkCanUpdateTeamEventType(user, eventTypeId, teamId, body.scheduleId);
123+
119124
const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user);
120125
const bookingFields = body.bookingFields ? [...body.bookingFields] : undefined;
121126

@@ -175,14 +180,31 @@ export class EventTypesAtomService {
175180
}
176181

177182
async checkCanUpdateTeamEventType(
178-
userId: number,
183+
user: UserWithProfile,
179184
eventTypeId: number,
180185
teamId: number,
181186
scheduleId: number | null | undefined
182187
) {
183-
await this.checkTeamOwnsEventType(userId, eventTypeId, teamId);
188+
const organizationId = this.usersService.getUserMainOrgId(user);
189+
190+
if (organizationId) {
191+
const isUserOrganizationAdmin = await this.membershipsRepository.isUserOrganizationAdmin(
192+
user.id,
193+
organizationId
194+
);
195+
196+
if (isUserOrganizationAdmin) {
197+
const orgTeam = await this.organizationsTeamsRepository.findOrgTeam(organizationId, teamId);
198+
if (orgTeam) {
199+
await this.teamEventTypeService.validateEventTypeExists(teamId, eventTypeId);
200+
return;
201+
}
202+
}
203+
}
204+
205+
await this.checkTeamOwnsEventType(user.id, eventTypeId, teamId);
184206
await this.teamEventTypeService.validateEventTypeExists(teamId, eventTypeId);
185-
await this.eventTypeService.checkUserOwnsSchedule(userId, scheduleId);
207+
await this.eventTypeService.checkUserOwnsSchedule(user.id, scheduleId);
186208
}
187209

188210
async checkTeamOwnsEventType(userId: number, eventTypeId: number, teamId: number) {

apps/api/v2/src/modules/organizations/memberships/services/organizations-membership.service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ export class OrganizationsMembershipService {
2727
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
2828
}
2929

30+
async isOrgAdminOrOwner(organizationId: number, userId: number) {
31+
const membership = await this.organizationsMembershipRepository.findOrgMembershipByUserId(
32+
organizationId,
33+
userId
34+
);
35+
if (!membership) {
36+
return false;
37+
}
38+
return membership.role === "ADMIN" || membership.role === "OWNER";
39+
}
40+
3041
async getOrgMembershipByUserId(organizationId: number, userId: number) {
3142
const membership = await this.organizationsMembershipRepository.findOrgMembershipByUserId(
3243
organizationId,

apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-a
1414
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
1515
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
1616
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
17+
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
1718
import { CreateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/create-organization-team.input";
1819
import { UpdateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/update-organization-team.input";
1920
import {
@@ -56,7 +57,10 @@ import { Team } from "@calcom/prisma/client";
5657
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
5758
@ApiHeader(OPTIONAL_API_KEY_HEADER)
5859
export class OrganizationsTeamsController {
59-
constructor(private organizationsTeamsService: OrganizationsTeamsService) {}
60+
constructor(
61+
private organizationsTeamsService: OrganizationsTeamsService,
62+
private organizationsMembershipService: OrganizationsMembershipService
63+
) {}
6064

6165
@Get()
6266
@ApiOperation({ summary: "Get all teams" })
@@ -84,12 +88,11 @@ export class OrganizationsTeamsController {
8488
@GetUser() user: UserWithProfile
8589
): Promise<OrgMeTeamsOutputResponseDto> {
8690
const { skip, take } = queryParams;
87-
const teams = await this.organizationsTeamsService.getPaginatedOrgUserTeams(
88-
orgId,
89-
user.id,
90-
skip ?? 0,
91-
take ?? 250
92-
);
91+
const isOrgAdminOrOwner = await this.organizationsMembershipService.isOrgAdminOrOwner(orgId, user.id);
92+
const teams = isOrgAdminOrOwner
93+
? await this.organizationsTeamsService.getPaginatedOrgTeamsWithMembers(orgId, skip ?? 0, take ?? 250)
94+
: await this.organizationsTeamsService.getPaginatedOrgUserTeams(orgId, user.id, skip ?? 0, take ?? 250);
95+
9396
return {
9497
status: SUCCESS_STATUS,
9598
data: teams.map((team) => {

apps/api/v2/src/modules/organizations/teams/index/organizations-teams.repository.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,17 @@ export class OrganizationsTeamsRepository {
107107
take,
108108
});
109109
}
110+
111+
async findOrgTeamsPaginatedWithMembers(organizationId: number, skip: number, take: number) {
112+
return this.dbRead.prisma.team.findMany({
113+
where: {
114+
parentId: organizationId,
115+
},
116+
include: {
117+
members: { select: { accepted: true, userId: true, role: true } },
118+
},
119+
skip,
120+
take,
121+
});
122+
}
110123
}

apps/api/v2/src/modules/organizations/teams/index/services/organizations-teams.service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ export class OrganizationsTeamsService {
2424
return teams;
2525
}
2626

27+
async getPaginatedOrgTeamsWithMembers(organizationId: number, skip = 0, take = 250) {
28+
const teams = await this.organizationsTeamRepository.findOrgTeamsPaginatedWithMembers(
29+
organizationId,
30+
skip,
31+
take
32+
);
33+
return teams;
34+
}
35+
2736
async getPaginatedOrgTeams(organizationId: number, skip = 0, take = 250) {
2837
const teams = await this.organizationsTeamRepository.findOrgTeamsPaginated(organizationId, skip, take);
2938
return teams;

0 commit comments

Comments
 (0)