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:
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;
|
||||
|
||||
+128
-8
@@ -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
-5
@@ -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 },
|
||||
|
||||
+10
-7
@@ -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;
|
||||
}
|
||||
+32
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user