refactor: v2 api event-types/:eventTypeId access (#24969)

* refactor: EventTypeAccess service

* feat: event-types/:id system admin access and team event access

* fix: implement cubic feedback

* fix: e2e

* fix: e2e

* fix: oasdiff ignore non-breaking change

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
Lauris SkraucisandUdit Takkar authored and GitHub committed 2025-11-07 21:17:16 +02:00
1 parent 81a99ea0b2
commit 829edec0ea
10 files changed
+725 -488

No files matched your search

@@ -1,5 +1,5 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
@@ -13,9 +13,8 @@ import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/ge
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { BillingService } from "@/modules/billing/services/billing.service";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service";
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { MembershipsService } from "@/modules/memberships/services/memberships.service";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
@@ -109,12 +108,11 @@ export class BookingsService_2024_08_13 {
private readonly organizationsRepository: OrganizationsRepository,
private readonly teamsRepository: TeamsRepository,
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
private readonly membershipsRepository: MembershipsRepository,
private readonly membershipsService: MembershipsService,
private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13,
private readonly regularBookingService: RegularBookingService,
private readonly recurringBookingService: RecurringBookingService,
private readonly instantBookingCreateService: InstantBookingCreateService
private readonly instantBookingCreateService: InstantBookingCreateService,
private readonly eventTypeAccessService: EventTypeAccessService
) {}
async createBooking(request: Request, body: CreateBookingInput, authUser: AuthOptionalUser) {
@@ -128,7 +126,7 @@ export class BookingsService_2024_08_13 {
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
}
const userIsEventTypeAdminOrOwner = authUser
? await this.userIsEventTypeAdminOrOwner(authUser, eventType)
? await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(authUser, eventType)
: false;
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser, userIsEventTypeAdminOrOwner);
@@ -197,44 +195,6 @@ export class BookingsService_2024_08_13 {
}
}
async userIsEventTypeAdminOrOwner(authUser: ApiAuthGuardUser, eventType: EventType) {
const authUserId = authUser.id;
const authUserRole = authUser.role;
const eventTypeId = eventType.id;
const teamId = eventType.teamId;
const eventTypeOwnerId = eventType.userId || null;
if (authUserRole === "ADMIN") return true;
if (eventTypeOwnerId === authUserId) return true;
if (eventTypeId) {
const [isUserHost, isUserAssigned] = await Promise.all([
this.eventTypesRepository.isUserHostOfEventType(authUserId, eventTypeId),
this.eventTypesRepository.isUserAssignedToEventType(authUserId, eventTypeId),
]);
if (isUserHost || isUserAssigned) return true;
}
if (teamId) {
const membership = await this.membershipsRepository.getUserAdminOrOwnerTeamMembership(
authUserId,
teamId
);
if (membership) return true;
}
if (
eventTypeOwnerId &&
(await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, eventTypeOwnerId))
) {
return true;
}
return false;
}
async getBookedEventType(body: CreateBookingInput) {
if (body.eventTypeId) {
return await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(body.eventTypeId);
@@ -616,7 +576,7 @@ export class BookingsService_2024_08_13 {
const booking = await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid);
const userIsEventTypeAdminOrOwner =
authUser && booking?.eventType
? await this.userIsEventTypeAdminOrOwner(authUser, booking.eventType)
? await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(authUser, booking.eventType)
: false;
if (booking) {
@@ -805,7 +765,7 @@ export class BookingsService_2024_08_13 {
const userIsEventTypeAdminOrOwner =
authUser && databaseBooking.eventType
? await this.userIsEventTypeAdminOrOwner(authUser, databaseBooking.eventType)
? await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(authUser, databaseBooking.eventType)
: false;
const isRecurring = !!databaseBooking.recurringEventId;
const isSeated = !!databaseBooking.eventType?.seatsPerTimeSlot;
@@ -22,17 +22,31 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
import { BookingWindowPeriodInputTypeEnum_2024_06_14, BookerLayoutsInputEnum_2024_06_14, ConfirmationPolicyEnum, NoticeThresholdUnitEnum, FrequencyInput } from "@calcom/platform-enums";
import {
BookingWindowPeriodInputTypeEnum_2024_06_14,
BookerLayoutsInputEnum_2024_06_14,
ConfirmationPolicyEnum,
NoticeThresholdUnitEnum,
FrequencyInput,
} from "@calcom/platform-enums";
import { SchedulingType } from "@calcom/platform-libraries";
import { type ApiSuccessResponse, type CreateEventTypeInput_2024_06_14, type EventTypeOutput_2024_06_14, type GuestsDefaultFieldOutput_2024_06_14, type NameDefaultFieldInput_2024_06_14, type NotesDefaultFieldInput_2024_06_14, type SplitNameDefaultFieldOutput_2024_06_14, type UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import {
BaseConfirmationPolicy_2024_06_14,
TeamEventTypeOutput_2024_06_14,
type ApiSuccessResponse,
type CreateEventTypeInput_2024_06_14,
type EventTypeOutput_2024_06_14,
type GuestsDefaultFieldOutput_2024_06_14,
type NameDefaultFieldInput_2024_06_14,
type NotesDefaultFieldInput_2024_06_14,
type SplitNameDefaultFieldOutput_2024_06_14,
type UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { FAILED_RECURRING_EVENT_TYPE_WITH_BOOKER_LIMITS_ERROR_MESSAGE } from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/validators/CantHaveRecurrenceAndBookerActiveBookingsLimit";
import { REQUIRES_AT_LEAST_ONE_PROPERTY_ERROR } from "@calcom/platform-types/utils/RequiresOneOfPropertiesWhenNotDisabled";
import type { PlatformOAuthClient, Team, User, Schedule, EventType } from "@calcom/prisma/client";
const orderBySlug = (a: { slug: string }, b: { slug: string }) => {
if (a.slug < b.slug) return -1;
if (a.slug > b.slug) return 1;
@@ -83,6 +97,8 @@ describe("Event types Endpoints", () => {
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let apiKeyString: string;
let apiKeyOrgUser: string;
let systemAdminUser: User;
let systemAdminApiKeyString: string;
const userEmail = `event-types-2024-06-14-user-${randomString()}@api.com`;
const falseTestEmail = `event-types-2024-06-14-false-user-${randomString()}@api.com`;
@@ -196,6 +212,17 @@ describe("Event types Endpoints", () => {
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = `cal_test_${keyString}`;
systemAdminUser = await userRepositoryFixture.create({
email: `event-types-2024-06-14-system-admin-${randomString()}@api.com`,
username: `event-types-2024-06-14-system-admin-${randomString()}`,
role: "ADMIN",
});
const { keyString: adminKeyString } = await apiKeysRepositoryFixture.createApiKey(
systemAdminUser.id,
null
);
systemAdminApiKeyString = `cal_test_${adminKeyString}`;
orgUser = await userRepositoryFixture.create({
email: `event-types-2024-06-14-org-user-${randomString()}@example.com`,
name: `event-types-2024-06-14-org-user-${randomString()}`,
@@ -1375,6 +1402,99 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType.color).toEqual(eventType.color);
});
it("system admin can access another user's event type by id", async () => {
return request(app.getHttpServer())
.get(`/api/v2/event-types/${orgUserEventType1.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${systemAdminApiKeyString}`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.id).toEqual(orgUserEventType1.id);
expect(responseBody.data.ownerId).toEqual(orgUser.id);
});
});
it("user can access a team event type as team member (using user's API key)", async () => {
const team = await teamRepositoryFixture.create({
name: `event-types-2024-06-14-team-${randomString()}`,
isOrganization: false,
});
await membershipsRepositoryFixture.create({
role: "ADMIN",
user: { connect: { id: user.id } },
team: { connect: { id: team.id } },
accepted: true,
});
const teamEventType = await eventTypesRepositoryFixture.createTeamEventType({
title: `event-types-2024-06-14-team-event-${randomString()}`,
slug: `event-types-2024-06-14-team-event-${randomString()}`,
length: 60,
locations: [],
schedulingType: "COLLECTIVE",
team: { connect: { id: team.id } },
});
return request(app.getHttpServer())
.get(`/api/v2/event-types/${teamEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.id).toEqual(teamEventType.id);
expect(responseBody.data.teamId).toEqual(team.id);
await teamRepositoryFixture.delete(team.id);
});
});
it("user can access a team event type as HOST even if membership role is MEMBER (using user's API key)", async () => {
const team = await teamRepositoryFixture.create({
name: `event-types-2024-06-14-host-team-${randomString()}`,
isOrganization: false,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: user.id } },
team: { connect: { id: team.id } },
accepted: true,
});
const teamEventType = await eventTypesRepositoryFixture.createTeamEventType({
title: `event-types-2024-06-14-host-event-${randomString()}`,
slug: `event-types-2024-06-14-host-event-${randomString()}`,
length: 60,
locations: [],
schedulingType: "COLLECTIVE",
team: { connect: { id: team.id } },
hosts: {
create: [
{
user: { connect: { id: user.id } },
},
],
},
});
return request(app.getHttpServer())
.get(`/api/v2/event-types/${teamEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.id).toEqual(teamEventType.id);
expect(responseBody.data.teamId).toEqual(team.id);
await teamRepositoryFixture.delete(team.id);
});
});
it(`/GET/event-types by username and eventSlug`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${username}&eventSlug=${eventType.slug}`)
@@ -1780,7 +1900,7 @@ describe("Event types Endpoints", () => {
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const updatedEventType = responseBody.data;
const policy = updatedEventType.confirmationPolicy as any;
const policy = updatedEventType.confirmationPolicy as BaseConfirmationPolicy_2024_06_14;
expect(policy?.type).toEqual(ConfirmationPolicyEnum.ALWAYS);
expect(policy?.blockUnconfirmedBookingsInBooker).toEqual(false);
expect(policy?.noticeThreshold).toBeUndefined();
@@ -1808,7 +1928,7 @@ describe("Event types Endpoints", () => {
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const updatedEventType = responseBody.data;
const policy = updatedEventType.confirmationPolicy as any;
const policy = updatedEventType.confirmationPolicy as BaseConfirmationPolicy_2024_06_14;
expect(policy?.type).toEqual(ConfirmationPolicyEnum.TIME);
expect(policy?.noticeThreshold).toEqual({
unit: NoticeThresholdUnitEnum.MINUTES,
@@ -2793,4 +2913,4 @@ describe("Event types Endpoints", () => {
await app.close();
});
});
});
});
@@ -23,6 +23,8 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
@@ -73,7 +75,8 @@ export class EventTypesController_2024_06_14 {
private readonly eventTypesService: EventTypesService_2024_06_14,
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe,
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe
) {}
@Post("/")
@@ -107,21 +110,36 @@ export class EventTypesController_2024_06_14 {
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Get an event type",
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
Access control: This endpoint fetches an event type by ID and returns it only if the authenticated user is authorized. Authorization is granted to:
- System admins
- The event type owner
- Hosts of the event type or users assigned to the event type
- Team admins/owners of the team that owns the team event type
- Organization admins/owners of the event type owner's organization
- Organization admins/owners of the team's parent organization
Note: Update and delete endpoints remain restricted to the event type owner only.`,
})
async getEventTypeById(
@Param("eventTypeId") eventTypeId: string,
@GetUser() user: UserWithProfile
@GetUser() user: ApiAuthGuardUser
): Promise<GetEventTypeOutput_2024_06_14> {
const eventType = await this.eventTypesService.getUserEventType(user.id, Number(eventTypeId));
const eventType = await this.eventTypesService.getEventTypeByIdIfAuthorized(user, Number(eventTypeId));
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
const responseEventType =
"hosts" in eventType
? await this.outputTeamEventTypesResponsePipe.transform(eventType)
: this.eventTypeResponseTransformPipe.transform(eventType);
return {
status: SUCCESS_STATUS,
data: this.eventTypeResponseTransformPipe.transform(eventType),
data: responseEventType,
};
}
@@ -10,10 +10,15 @@ import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/output.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository } from "@/modules/users/users.repository";
@@ -26,6 +31,8 @@ import { Module } from "@nestjs/common";
EventTypesService_2024_06_14,
InputEventTypesService_2024_06_14,
OutputEventTypesService_2024_06_14,
EventTypeAccessService,
TeamsRepository,
UsersRepository,
UsersService,
SchedulesRepository_2024_06_11,
@@ -35,6 +42,9 @@ import { Module } from "@nestjs/common";
CredentialsRepository,
AppsRepository,
CalendarsRepository,
OutputTeamEventTypesResponsePipe,
OutputOrganizationsEventTypesService,
TeamsEventTypesRepository,
],
controllers: [EventTypesController_2024_06_14],
exports: [
@@ -42,6 +52,7 @@ import { Module } from "@nestjs/common";
EventTypesRepository_2024_06_14,
InputEventTypesService_2024_06_14,
OutputEventTypesService_2024_06_14,
EventTypeAccessService,
],
})
export class EventTypesModule_2024_06_14 {}
@@ -93,6 +93,19 @@ export class EventTypesRepository_2024_06_14 {
});
}
async getEventTypeByIdWithHosts(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: {
users: true,
schedule: true,
destinationCalendar: true,
calVideoSettings: true,
hosts: true,
},
});
}
async getEventTypeByIdIncludeUsersAndTeam(eventTypeId: number) {
const eventType = await this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
@@ -1,19 +1,22 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsIn, ValidateNested } from "class-validator";
import { IsIn } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types";
import { EventTypeOutput_2024_06_14, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
@ApiExtraModels(EventTypeOutput_2024_06_14, TeamEventTypeOutput_2024_06_14)
export class GetEventTypeOutput_2024_06_14 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsIn([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: EventTypeOutput_2024_06_14,
oneOf: [
{ $ref: getSchemaPath(EventTypeOutput_2024_06_14) },
{ $ref: getSchemaPath(TeamEventTypeOutput_2024_06_14) },
],
})
@ValidateNested()
@Type(() => EventTypeOutput_2024_06_14)
data!: EventTypeOutput_2024_06_14 | null;
@Type(() => Object)
data!: EventTypeOutput_2024_06_14 | TeamEventTypeOutput_2024_06_14 | null;
}
@@ -1,10 +1,14 @@
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_06_14/constants/constants";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { DatabaseEventType } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed";
import { SystemField, CustomField } from "@/ee/event-types/event-types_2024_06_14/transformers";
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { UsersService } from "@/modules/users/services/users.service";
@@ -30,7 +34,8 @@ export class EventTypesService_2024_06_14 {
private readonly usersService: UsersService,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
private readonly dbWrite: PrismaWriteService,
private readonly schedulesRepository: SchedulesRepository_2024_06_11
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
private readonly eventTypeAccessService: EventTypeAccessService
) {}
async createUserEventType(user: UserWithProfile, body: InputEventTransformed_2024_06_14) {
@@ -39,7 +44,7 @@ export class EventTypesService_2024_06_14 {
}
await this.checkCanCreateEventType(user.id, body);
const eventTypeUser = await this.getUserToCreateEvent(user);
const { destinationCalendar: _destinationCalendar, ...rest } = body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -79,6 +84,31 @@ export class EventTypesService_2024_06_14 {
};
}
async getEventTypeByIdIfAuthorized(
authUser: ApiAuthGuardUser,
eventTypeId: number
): Promise<DatabaseTeamEventType | ({ ownerId: number } & DatabaseEventType) | null> {
const eventType = await this.eventTypesRepository.getEventTypeByIdWithHosts(eventTypeId);
if (!eventType) {
return null;
}
const hasAccess = await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(
authUser,
eventType as unknown as EventType
);
if (!hasAccess) {
return null;
}
return {
ownerId: eventType.userId ?? 0,
...eventType,
};
}
async checkCanCreateEventType(userId: number, body: InputEventTransformed_2024_06_14) {
const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug);
if (existsWithSlug) {
@@ -0,0 +1,78 @@
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { MembershipsService } from "@/modules/memberships/services/memberships.service";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { Injectable } from "@nestjs/common";
import type { EventType } from "@calcom/prisma/client";
@Injectable()
export class EventTypeAccessService {
constructor(
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly membershipsRepository: MembershipsRepository,
private readonly membershipsService: MembershipsService,
private readonly teamsRepository: TeamsRepository
) {}
async userIsEventTypeAdminOrOwner(authUser: ApiAuthGuardUser, eventType: EventType): Promise<boolean> {
const authUserId = authUser.id;
const eventTypeId = eventType.id;
const teamId = eventType.teamId;
const eventTypeOwnerId = eventType.userId || null;
if (authUser.isSystemAdmin) return true;
if (eventTypeOwnerId === authUserId) return true;
if (eventTypeId) {
const isHostOrAssigned = await this.isUserHostOrAssignedToEventType(authUserId, eventTypeId);
if (isHostOrAssigned) return true;
}
if (teamId) {
const isTeamOrParentOrgAdmin = await this.isUserTeamAdminOrParentOrgAdmin(authUserId, teamId);
if (isTeamOrParentOrgAdmin) return true;
}
if (eventTypeOwnerId) {
const isOrgAdminOrOwnerOfEventOwner = await this.isUserOrgAdminOrOwnerOfEventOwner(
authUserId,
eventTypeOwnerId
);
if (isOrgAdminOrOwnerOfEventOwner) return true;
}
return false;
}
private async isUserHostOrAssignedToEventType(authUserId: number, eventTypeId: number): Promise<boolean> {
const [isUserHost, isUserAssigned] = await Promise.all([
this.eventTypesRepository.isUserHostOfEventType(authUserId, eventTypeId),
this.eventTypesRepository.isUserAssignedToEventType(authUserId, eventTypeId),
]);
return isUserHost || isUserAssigned;
}
private async isUserTeamAdminOrParentOrgAdmin(authUserId: number, teamId: number): Promise<boolean> {
const membership = await this.membershipsRepository.getUserAdminOrOwnerTeamMembership(authUserId, teamId);
if (membership) return true;
const team = await this.teamsRepository.getById(teamId);
const parentOrgId = team?.parentId ?? null;
if (parentOrgId) {
const isOrgAdmin = await this.membershipsRepository.isUserOrganizationAdmin(authUserId, parentOrgId);
if (isOrgAdmin) return true;
}
return false;
}
private async isUserOrgAdminOrOwnerOfEventOwner(
authUserId: number,
eventTypeOwnerId: number
): Promise<boolean> {
return this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, eventTypeOwnerId);
}
}