feat: add session endpoint and booking access pbac (#24637)
* feat: add session endpoint * fix: remove participant id * refactor; feedback * fix: e2e test * refactor: add pbac * refactor: use constant * tests: add booking tests * fix: conflicts * chore: test * fix: tests * fix: tests * chore: remove un necessary * chore: docs * fix: desc * fix: booking-pbac gurd * fix: booking-pbac gurd * test: add unit tests * fix: remove legacy pbac * fix: chore remove * fix: update tests * test: add more test * test: move it to new file * chore: comment auth * chore: add message * chore: add message --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
@@ -1,2 +1,4 @@
|
||||
GET /v2/event-types/{eventTypeId} added to the 'data' response property 'oneOf' list for the response status '200'
|
||||
GET /v2/event-types/{eventTypeId} added '#/components/schemas/EventTypeOutput_2024_06_14, #/components/schemas/TeamEventTypeOutput_2024_06_14' to the 'data' response property 'oneOf' list for the response status '200'
|
||||
GET /v2/bookings/{bookingUid}/recordings added the new required 'header' request parameter 'Authorization'
|
||||
GET /v2/bookings/{bookingUid}/transcripts added the new required 'header' request parameter 'Authorization'
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { BookingGuestsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-guests.controller";
|
||||
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
||||
import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard";
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { BookingGuestsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-guests.service";
|
||||
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { CalVideoOutputService } from "@/ee/bookings/2024-08-13/services/cal-video.output.service";
|
||||
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||
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";
|
||||
@@ -96,6 +98,8 @@ import { Module } from "@nestjs/common";
|
||||
BookingReferencesService_2024_08_13,
|
||||
BookingReferencesRepository_2024_08_13,
|
||||
CalVideoService,
|
||||
CalVideoOutputService,
|
||||
BookingPbacGuard,
|
||||
],
|
||||
controllers: [BookingsController_2024_08_13, BookingGuestsController_2024_08_13],
|
||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard";
|
||||
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
||||
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
|
||||
import { BookingReferencesOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/booking-references.output";
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
GetOptionalUser,
|
||||
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Pbac } from "@/modules/auth/decorators/pbac/pbac.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
|
||||
@@ -66,6 +68,7 @@ import {
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
GetBookingRecordingsOutput,
|
||||
GetBookingTranscriptsOutput,
|
||||
GetBookingVideoSessionsOutput,
|
||||
} from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInputPipe,
|
||||
@@ -209,12 +212,16 @@ export class BookingsController_2024_08_13 {
|
||||
}
|
||||
|
||||
@Get("/:bookingUid/recordings")
|
||||
// @Pbac(["booking.readRecordings"])
|
||||
@Permissions([BOOKING_READ])
|
||||
@UseGuards(BookingUidGuard)
|
||||
// @UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Get all the recordings for the booking",
|
||||
description: `Fetches all the recordings for the booking \`:bookingUid\`
|
||||
description: `Fetches all the recordings for the booking \`:bookingUid\`. Requires authentication and proper authorization. Access is granted if you are the booking organizer, team admin or org admin/owner.
|
||||
|
||||
<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>
|
||||
<Note>cal-api-version: \`2024-08-13\` is required in the request header.</Note>
|
||||
`,
|
||||
})
|
||||
async getBookingRecordings(@Param("bookingUid") bookingUid: string): Promise<GetBookingRecordingsOutput> {
|
||||
@@ -223,11 +230,17 @@ export class BookingsController_2024_08_13 {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: recordings,
|
||||
message:
|
||||
"This endpoint will require authentication in a future release. Please update your integration to include valid credentials. See https://cal.com/docs/api-reference/v2/introduction#authentication for details.",
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:bookingUid/transcripts")
|
||||
// @Pbac(["booking.readRecordings"])
|
||||
@Permissions([BOOKING_READ])
|
||||
@UseGuards(BookingUidGuard)
|
||||
// @UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Get Cal Video real time transcript download links for the booking",
|
||||
description: `Fetches all the transcript download links for the booking \`:bookingUid\`
|
||||
@@ -245,6 +258,8 @@ export class BookingsController_2024_08_13 {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: transcripts ?? [],
|
||||
message:
|
||||
"This endpoint will require authentication in a future release. Please update your integration to include valid credentials. See https://cal.com/docs/api-reference/v2/introduction#authentication for details.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -546,4 +561,25 @@ export class BookingsController_2024_08_13 {
|
||||
data: bookingReferences,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:bookingUid/conferencing-sessions")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Pbac(["booking.readRecordings"])
|
||||
@Permissions([BOOKING_READ])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Get Video Meeting Sessions. Only supported for Cal Video",
|
||||
description: `Requires authentication and proper authorization. Access is granted if you are the booking organizer, team admin or org admin/owner.
|
||||
|
||||
<Note>cal-api-version: \`2024-08-13\` is required in the request header.</Note>`,
|
||||
})
|
||||
async getVideoSessions(@Param("bookingUid") bookingUid: string): Promise<GetBookingVideoSessionsOutput> {
|
||||
const sessions = await this.calVideoService.getVideoSessions(bookingUid);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: sessions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import type { Booking, User, PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
describe("Booking access authorization", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
|
||||
const ownerEmail = `booking-access-auth-owner-${randomString()}@api.com`;
|
||||
const unauthorizedEmail = `booking-access-auth-unauthorized-${randomString()}@api.com`;
|
||||
let ownerUser: User;
|
||||
let unauthorizedUser: User;
|
||||
let ownerApiKey: string;
|
||||
let unauthorizedApiKey: string;
|
||||
|
||||
let eventTypeId: number;
|
||||
let testBooking: Booking;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await organizationsRepositoryFixture.create({
|
||||
name: `booking-access-auth-organization-${randomString()}`,
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
ownerUser = await userRepositoryFixture.create({
|
||||
email: ownerEmail,
|
||||
locale: "en",
|
||||
name: `booking-access-auth-owner-${randomString()}`,
|
||||
});
|
||||
|
||||
unauthorizedUser = await userRepositoryFixture.create({
|
||||
email: unauthorizedEmail,
|
||||
locale: "en",
|
||||
name: `booking-access-auth-unauthorized-${randomString()}`,
|
||||
});
|
||||
|
||||
const { keyString: ownerKeyString } = await apiKeysRepositoryFixture.createApiKey(ownerUser.id, null);
|
||||
ownerApiKey = `cal_test_${ownerKeyString}`;
|
||||
|
||||
const { keyString: unauthorizedKeyString } = await apiKeysRepositoryFixture.createApiKey(
|
||||
unauthorizedUser.id,
|
||||
null
|
||||
);
|
||||
unauthorizedApiKey = `cal_test_${unauthorizedKeyString}`;
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `booking-access-auth-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(ownerUser.id, userSchedule);
|
||||
|
||||
const eventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `booking-access-auth-event-type-${randomString()}`,
|
||||
slug: `booking-access-auth-event-type-${randomString()}`,
|
||||
length: 60,
|
||||
},
|
||||
ownerUser.id
|
||||
);
|
||||
eventTypeId = eventType.id;
|
||||
|
||||
testBooking = await bookingsRepositoryFixture.create({
|
||||
uid: `booking-access-auth-booking-${randomString()}`,
|
||||
title: "Test Booking for Access Auth",
|
||||
description: "",
|
||||
startTime: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 8, 11, 0, 0)),
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: ownerUser.id,
|
||||
},
|
||||
},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Test Attendee",
|
||||
email: "attendee@example.com",
|
||||
},
|
||||
references: {
|
||||
create: [
|
||||
{
|
||||
type: "daily_video",
|
||||
uid: `daily-room-${randomString()}`,
|
||||
meetingId: `daily-room-${randomString()}`,
|
||||
meetingPassword: "test-password",
|
||||
meetingUrl: `https://daily.co/test-room-${randomString()}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("GET /v2/bookings/:bookingUid/conferencing-sessions - Authorization", () => {
|
||||
it("should allow booking organizer to access conferencing sessions", async () => {
|
||||
const calVideoService = app.get(CalVideoService);
|
||||
jest.spyOn(calVideoService, "getVideoSessions").mockResolvedValue([]);
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testBooking.uid}/conferencing-sessions`)
|
||||
.set("Authorization", `Bearer ${ownerApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
});
|
||||
|
||||
it("should return 403 when unauthorized user tries to access conferencing sessions", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testBooking.uid}/conferencing-sessions`)
|
||||
.set("Authorization", `Bearer ${unauthorizedApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
// describe("GET /v2/bookings/:bookingUid/recordings - Authorization", () => {
|
||||
// it("should allow booking organizer to access recordings", async () => {
|
||||
// const calVideoService = app.get(CalVideoService);
|
||||
// jest.spyOn(calVideoService, "getRecordings").mockResolvedValue([]);
|
||||
|
||||
// const response = await request(app.getHttpServer())
|
||||
// .get(`/v2/bookings/${testBooking.uid}/recordings`)
|
||||
// .set("Authorization", `Bearer ${ownerApiKey}`)
|
||||
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
// .expect(200);
|
||||
|
||||
// expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
// });
|
||||
|
||||
// it("should return 403 when unauthorized user tries to access recordings", async () => {
|
||||
// await request(app.getHttpServer())
|
||||
// .get(`/v2/bookings/${testBooking.uid}/recordings`)
|
||||
// .set("Authorization", `Bearer ${unauthorizedApiKey}`)
|
||||
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
// .expect(403);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe("GET /v2/bookings/:bookingUid/transcripts - Authorization", () => {
|
||||
// it("should allow booking organizer to access transcripts", async () => {
|
||||
// const calVideoService = app.get(CalVideoService);
|
||||
// jest.spyOn(calVideoService, "getTranscripts").mockResolvedValue([]);
|
||||
|
||||
// const response = await request(app.getHttpServer())
|
||||
// .get(`/v2/bookings/${testBooking.uid}/transcripts`)
|
||||
// .set("Authorization", `Bearer ${ownerApiKey}`)
|
||||
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
// .expect(200);
|
||||
|
||||
// expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
// });
|
||||
|
||||
// it("should return 403 when unauthorized user tries to access transcripts", async () => {
|
||||
// await request(app.getHttpServer())
|
||||
// .get(`/v2/bookings/${testBooking.uid}/transcripts`)
|
||||
// .set("Authorization", `Bearer ${unauthorizedApiKey}`)
|
||||
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
// .expect(403);
|
||||
// });
|
||||
// });
|
||||
|
||||
afterAll(async () => {
|
||||
await bookingsRepositoryFixture.deleteById(testBooking.id);
|
||||
await eventTypesRepositoryFixture.delete(eventTypeId);
|
||||
await userRepositoryFixture.deleteByEmail(ownerEmail);
|
||||
await userRepositoryFixture.deleteByEmail(unauthorizedEmail);
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||
import { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
@@ -3078,6 +3079,102 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
);
|
||||
}
|
||||
|
||||
describe("Meeting sessions", () => {
|
||||
it("should get cal video sessions for a booking", async () => {
|
||||
const mockSessions = [
|
||||
{
|
||||
id: "session-123",
|
||||
room: "daily-room-123",
|
||||
start_time: 1678901234,
|
||||
duration: 3600,
|
||||
ongoing: false,
|
||||
max_participants: 10,
|
||||
participants: [
|
||||
{
|
||||
user_id: "user-1",
|
||||
participant_id: "participant-1",
|
||||
user_name: "John Doe",
|
||||
join_time: 1678901234,
|
||||
duration: 3600,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const booking = await bookingsRepositoryFixture.create({
|
||||
uid: `test-video-session-${randomString()}`,
|
||||
title: "Test Video Session Booking",
|
||||
description: "",
|
||||
startTime: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 8, 11, 0, 0)),
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
references: {
|
||||
create: [
|
||||
{
|
||||
type: "daily_video",
|
||||
uid: `daily-room-123-${randomString()}`,
|
||||
meetingId: "daily-room-123",
|
||||
meetingPassword: "test-password",
|
||||
meetingUrl: "https://daily.co/daily-room-123",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const calVideoService = app.get(CalVideoService);
|
||||
jest
|
||||
.spyOn(calVideoService, "getVideoSessions")
|
||||
.mockResolvedValue([
|
||||
{
|
||||
id: mockSessions[0].id,
|
||||
room: mockSessions[0].room,
|
||||
startTime: mockSessions[0].start_time,
|
||||
duration: mockSessions[0].duration,
|
||||
ongoing: mockSessions[0].ongoing,
|
||||
maxParticipants: mockSessions[0].max_participants,
|
||||
participants: mockSessions[0].participants.map((p) => ({
|
||||
userId: p.user_id,
|
||||
userName: p.user_name,
|
||||
joinTime: p.join_time,
|
||||
duration: p.duration,
|
||||
})),
|
||||
},
|
||||
]);
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${booking.uid}/conferencing-sessions`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
expect(response.body.data.length).toBe(1);
|
||||
expect(response.body.data[0].id).toEqual(mockSessions[0].id);
|
||||
expect(response.body.data[0].room).toEqual(mockSessions[0].room);
|
||||
expect(response.body.data[0].startTime).toEqual(mockSessions[0].start_time);
|
||||
expect(response.body.data[0].duration).toEqual(mockSessions[0].duration);
|
||||
expect(response.body.data[0].ongoing).toEqual(mockSessions[0].ongoing);
|
||||
expect(response.body.data[0].maxParticipants).toEqual(mockSessions[0].max_participants);
|
||||
expect(response.body.data[0].participants.length).toBe(1);
|
||||
expect(response.body.data[0].participants[0].userId).toEqual(mockSessions[0].participants[0].user_id);
|
||||
expect(response.body.data[0].participants[0].userName).toEqual(
|
||||
mockSessions[0].participants[0].user_name
|
||||
);
|
||||
|
||||
await bookingsRepositoryFixture.deleteById(booking.id);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
UnauthorizedException,
|
||||
BadRequestException,
|
||||
} from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { BookingAccessService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class BookingPbacGuard implements CanActivate {
|
||||
private bookingAccessService: BookingAccessService;
|
||||
|
||||
constructor(private readonly prismaReadService: PrismaReadService) {
|
||||
this.bookingAccessService = new BookingAccessService(this.prismaReadService.prisma);
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context
|
||||
.switchToHttp()
|
||||
.getRequest<Request & { user?: ApiAuthGuardUser; pbacAuthorizedRequest?: boolean }>();
|
||||
const user = request.user;
|
||||
const bookingUid = request.params.bookingUid;
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
if (!bookingUid) {
|
||||
throw new BadRequestException("BookingPbacGuard - bookingUid is required");
|
||||
}
|
||||
|
||||
const hasAccess = await this.bookingAccessService.doesUserIdHaveAccessToBooking({
|
||||
userId: user.id,
|
||||
bookingUid,
|
||||
});
|
||||
|
||||
if (!hasAccess) {
|
||||
throw new ForbiddenException(
|
||||
`BookingPbacGuard - user with id=${user.id} does not have access to booking with uid=${bookingUid}`
|
||||
);
|
||||
}
|
||||
|
||||
request.pbacAuthorizedRequest = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import type { CalMeetingSession } from "@calcom/platform-libraries/conferencing";
|
||||
|
||||
@Injectable()
|
||||
export class CalVideoOutputService {
|
||||
getOutputVideoSessions(sessions: CalMeetingSession[]) {
|
||||
return sessions.map((session) => ({
|
||||
id: session.id,
|
||||
room: session.room,
|
||||
startTime: session.start_time,
|
||||
duration: session.duration,
|
||||
ongoing: session.ongoing,
|
||||
maxParticipants: session.max_participants,
|
||||
participants: session.participants.map((participant) => ({
|
||||
userId: participant.user_id,
|
||||
userName: participant.user_name,
|
||||
joinTime: participant.join_time,
|
||||
duration: participant.duration,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { CalVideoOutputService } from "@/ee/bookings/2024-08-13/services/cal-video.output.service";
|
||||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { CAL_VIDEO_TYPE } from "@calcom/platform-constants";
|
||||
import {
|
||||
getRecordingsOfCalVideoByRoomName,
|
||||
getAllTranscriptsAccessLinkFromRoomName,
|
||||
getDownloadLinkOfCalVideoByRecordingId,
|
||||
getCalVideoMeetingSessionsByRoomName,
|
||||
} from "@calcom/platform-libraries/conferencing";
|
||||
|
||||
@Injectable()
|
||||
@@ -13,18 +15,23 @@ export class CalVideoService {
|
||||
private readonly logger = new Logger("CalVideoService");
|
||||
constructor(
|
||||
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||
private readonly outputService: OutputBookingsService_2024_08_13
|
||||
private readonly calVideoOutputService: CalVideoOutputService
|
||||
) {}
|
||||
|
||||
private getVideoSessionsRoomName(references?: Array<{ type: string; meetingId?: string | null }>) {
|
||||
return (
|
||||
references?.filter((reference) => reference.type === CAL_VIDEO_TYPE)?.pop()?.meetingId ??
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
async getRecordings(bookingUid: string) {
|
||||
const booking = await this.bookingsRepository.getByUidWithBookingReference(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const roomName =
|
||||
booking?.references?.filter((reference) => reference.type === "daily_video")?.pop()?.meetingId ??
|
||||
undefined;
|
||||
const roomName = this.getVideoSessionsRoomName(booking.references);
|
||||
if (!roomName) {
|
||||
throw new NotFoundException(`No Cal Video reference found with booking uid ${bookingUid}`);
|
||||
}
|
||||
@@ -68,10 +75,7 @@ export class CalVideoService {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const roomName =
|
||||
booking?.references?.filter((reference) => reference.type === "daily_video")?.pop()?.meetingId ??
|
||||
undefined;
|
||||
|
||||
const roomName = this.getVideoSessionsRoomName(booking.references);
|
||||
if (!roomName) {
|
||||
throw new NotFoundException(`No Cal Video reference found with booking uid ${bookingUid}`);
|
||||
}
|
||||
@@ -80,4 +84,19 @@ export class CalVideoService {
|
||||
|
||||
return transcripts;
|
||||
}
|
||||
|
||||
async getVideoSessions(bookingUid: string) {
|
||||
const booking = await this.bookingsRepository.getByUidWithBookingReference(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const roomName = this.getVideoSessionsRoomName(booking.references);
|
||||
if (!roomName) {
|
||||
throw new NotFoundException(`No Cal Video reference found with booking uid ${bookingUid}`);
|
||||
}
|
||||
|
||||
const sessions = await getCalVideoMeetingSessionsByRoomName(roomName);
|
||||
return this.calVideoOutputService.getOutputVideoSessions(sessions.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9903,7 +9903,7 @@
|
||||
"get": {
|
||||
"operationId": "BookingsController_2024_08_13_getBookingRecordings",
|
||||
"summary": "Get all the recordings for the booking",
|
||||
"description": "Fetches all the recordings for the booking `:bookingUid`\n\n <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>\n ",
|
||||
"description": "Fetches all the recordings for the booking `:bookingUid`. Requires authentication and proper authorization. Access is granted if you are the booking organizer, team admin or org admin/owner.\n\n <Note>cal-api-version: `2024-08-13` is required in the request header.</Note>\n ",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
@@ -9922,6 +9922,15 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -9962,6 +9971,15 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -10548,6 +10566,55 @@
|
||||
"tags": ["Bookings"]
|
||||
}
|
||||
},
|
||||
"/v2/bookings/{bookingUid}/conferencing-sessions": {
|
||||
"get": {
|
||||
"operationId": "BookingsController_2024_08_13_getVideoSessions",
|
||||
"summary": "Get Video Meeting Sessions. Only supported for Cal Video",
|
||||
"description": "Requires authentication and proper authorization. Access is granted if you are the booking organizer, team admin or org admin/owner.\n\n <Note>cal-api-version: `2024-08-13` is required in the request header.</Note>",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to 2024-08-13. If not set to this value, the endpoint will default to an older version.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "2024-08-13"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bookingUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetBookingVideoSessionsOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["Bookings"]
|
||||
}
|
||||
},
|
||||
"/v2/bookings/{bookingUid}/guests": {
|
||||
"post": {
|
||||
"operationId": "BookingGuestsController_2024_08_13_addGuests",
|
||||
@@ -29688,6 +29755,86 @@
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"CalMeetingParticipant": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"userId": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"example": "user123"
|
||||
},
|
||||
"userName": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"example": "John Doe"
|
||||
},
|
||||
"joinTime": {
|
||||
"type": "number",
|
||||
"example": 1678901234
|
||||
},
|
||||
"duration": {
|
||||
"type": "number",
|
||||
"example": 3600
|
||||
}
|
||||
},
|
||||
"required": ["userId", "userName", "joinTime", "duration"]
|
||||
},
|
||||
"CalMeetingSession": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"example": "session123"
|
||||
},
|
||||
"room": {
|
||||
"type": "string",
|
||||
"example": "daily-video-room-123"
|
||||
},
|
||||
"startTime": {
|
||||
"type": "number",
|
||||
"example": 1678901234
|
||||
},
|
||||
"duration": {
|
||||
"type": "number",
|
||||
"example": 3600
|
||||
},
|
||||
"ongoing": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"maxParticipants": {
|
||||
"type": "number",
|
||||
"example": 10
|
||||
},
|
||||
"participants": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CalMeetingParticipant"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["id", "room", "startTime", "duration", "ongoing", "maxParticipants", "participants"]
|
||||
},
|
||||
"GetBookingVideoSessionsOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": ["success", "error"]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CalMeetingSession"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"Guest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -13,7 +13,12 @@ import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
|
||||
|
||||
import { ZSubmitBatchProcessorJobRes, ZGetTranscriptAccessLink } from "../zod";
|
||||
import {
|
||||
ZSubmitBatchProcessorJobRes,
|
||||
ZGetTranscriptAccessLink,
|
||||
getMeetingInformationResponseSchema,
|
||||
TGetMeetingInformationResponsesSchema,
|
||||
} from "../zod";
|
||||
import type { TSubmitBatchProcessorJobRes, TGetTranscriptAccessLink, batchProcessorBody } from "../zod";
|
||||
import { fetcher } from "./dailyApiFetcher";
|
||||
import {
|
||||
@@ -25,28 +30,6 @@ import {
|
||||
ZGetMeetingTokenResponseSchema,
|
||||
} from "./types";
|
||||
|
||||
const meetingParticipantSchema = z.object({
|
||||
user_id: z.string().nullable(),
|
||||
participant_id: z.string(),
|
||||
user_name: z.string().nullable(),
|
||||
join_time: z.number(),
|
||||
duration: z.number(),
|
||||
});
|
||||
|
||||
const meetingSessionSchema = z.object({
|
||||
id: z.string(),
|
||||
room: z.string(),
|
||||
start_time: z.number(),
|
||||
duration: z.number(),
|
||||
ongoing: z.boolean(),
|
||||
max_participants: z.number(),
|
||||
participants: z.array(meetingParticipantSchema),
|
||||
});
|
||||
|
||||
const getMeetingInformationResponseSchema = z.object({
|
||||
data: z.array(meetingSessionSchema),
|
||||
});
|
||||
|
||||
export interface DailyEventResult {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -520,7 +503,7 @@ const DailyVideoApiAdapter = (): VideoApiAdapter => {
|
||||
throw new Error(`Something went wrong! Unable to checkIfRoomNameMatchesInRecording. ${err}`);
|
||||
}
|
||||
},
|
||||
getMeetingInformation: async (roomName: string) => {
|
||||
getMeetingInformation: async (roomName: string): Promise<TGetMeetingInformationResponsesSchema> => {
|
||||
try {
|
||||
const res = await fetcher(`/meetings?room=${encodeURIComponent(roomName)}`).then(
|
||||
getMeetingInformationResponseSchema.parse
|
||||
|
||||
@@ -38,4 +38,30 @@ export const ZGetTranscriptAccessLink = z.object({
|
||||
),
|
||||
});
|
||||
|
||||
const meetingParticipantSchema = z.object({
|
||||
user_id: z.string().nullable(),
|
||||
participant_id: z.string(),
|
||||
user_name: z.string().nullable(),
|
||||
join_time: z.number(),
|
||||
duration: z.number(),
|
||||
});
|
||||
|
||||
const meetingSessionSchema = z.object({
|
||||
id: z.string(),
|
||||
room: z.string(),
|
||||
start_time: z.number(),
|
||||
duration: z.number(),
|
||||
ongoing: z.boolean(),
|
||||
max_participants: z.number(),
|
||||
participants: z.array(meetingParticipantSchema),
|
||||
});
|
||||
|
||||
export const getMeetingInformationResponseSchema = z.object({
|
||||
data: z.array(meetingSessionSchema),
|
||||
});
|
||||
|
||||
export type CalMeetingParticipant = z.infer<typeof meetingParticipantSchema>;
|
||||
export type CalMeetingSession = z.infer<typeof meetingSessionSchema>;
|
||||
export type TGetMeetingInformationResponsesSchema = z.infer<typeof getMeetingInformationResponseSchema>;
|
||||
|
||||
export type TGetTranscriptAccessLink = z.infer<typeof ZGetTranscriptAccessLink>;
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { BookingRepository } from "../repositories/BookingRepository";
|
||||
import { BookingAccessService } from "./BookingAccessService";
|
||||
|
||||
vi.mock("../repositories/BookingRepository");
|
||||
vi.mock("@calcom/features/users/repositories/UserRepository");
|
||||
|
||||
describe("BookingAccessService", () => {
|
||||
let service: BookingAccessService;
|
||||
let mockPrismaClient: PrismaClient;
|
||||
let mockBookingRepo: {
|
||||
findByUidIncludeEventType: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let mockUserRepo: {
|
||||
getUserOrganizationAndTeams: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let mockPermissionCheckService: {
|
||||
checkPermission: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockPrismaClient = {} as PrismaClient;
|
||||
|
||||
mockBookingRepo = {
|
||||
findByUidIncludeEventType: vi.fn(),
|
||||
};
|
||||
|
||||
mockUserRepo = {
|
||||
getUserOrganizationAndTeams: vi.fn(),
|
||||
};
|
||||
|
||||
mockPermissionCheckService = {
|
||||
checkPermission: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mocked(BookingRepository).mockImplementation(() => mockBookingRepo as any);
|
||||
vi.mocked(UserRepository).mockImplementation(() => mockUserRepo as any);
|
||||
|
||||
service = new BookingAccessService(mockPrismaClient);
|
||||
|
||||
(service as any).permissionCheckService = mockPermissionCheckService;
|
||||
});
|
||||
|
||||
describe("doesUserIdHaveAccessToBooking", () => {
|
||||
describe("Case 1: Booking Organizer", () => {
|
||||
it("should return true when user is the booking organizer", async () => {
|
||||
const mockBooking = {
|
||||
userId: 123,
|
||||
eventType: null,
|
||||
attendees: [],
|
||||
};
|
||||
|
||||
mockBookingRepo.findByUidIncludeEventType.mockResolvedValue(mockBooking);
|
||||
|
||||
const result = await service.doesUserIdHaveAccessToBooking({
|
||||
userId: 123,
|
||||
bookingUid: "test-booking-uid",
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockBookingRepo.findByUidIncludeEventType).toHaveBeenCalledWith({
|
||||
bookingUid: "test-booking-uid",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false when user is not the organizer and booking has no team", async () => {
|
||||
const mockBooking = {
|
||||
userId: 456,
|
||||
eventType: null,
|
||||
attendees: [],
|
||||
};
|
||||
|
||||
mockBookingRepo.findByUidIncludeEventType.mockResolvedValue(mockBooking);
|
||||
mockUserRepo.getUserOrganizationAndTeams.mockResolvedValue(null);
|
||||
|
||||
const result = await service.doesUserIdHaveAccessToBooking({
|
||||
userId: 123,
|
||||
bookingUid: "test-booking-uid",
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Case 2: Booking Host", () => {
|
||||
it("should return true when user is a host in eventType.hosts", async () => {
|
||||
const mockBooking = {
|
||||
userId: 456,
|
||||
user: { id: 456, email: "organizer@example.com" },
|
||||
eventType: {
|
||||
hosts: [
|
||||
{ userId: 123, user: { email: "host@example.com" } },
|
||||
{ userId: 789, user: { email: "other-host@example.com" } },
|
||||
],
|
||||
users: [],
|
||||
},
|
||||
attendees: [{ email: "host@example.com" }],
|
||||
};
|
||||
|
||||
mockBookingRepo.findByUidIncludeEventType.mockResolvedValue(mockBooking);
|
||||
|
||||
const result = await service.doesUserIdHaveAccessToBooking({
|
||||
userId: 123,
|
||||
bookingUid: "test-booking-uid",
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when user is in eventType.users", async () => {
|
||||
const mockBooking = {
|
||||
userId: 456,
|
||||
user: { id: 456, email: "organizer@example.com" },
|
||||
eventType: {
|
||||
hosts: [],
|
||||
users: [
|
||||
{ id: 123, email: "user@example.com" },
|
||||
{ id: 789, email: "other-user@example.com" },
|
||||
],
|
||||
},
|
||||
attendees: [{ email: "user@example.com" }],
|
||||
};
|
||||
|
||||
mockBookingRepo.findByUidIncludeEventType.mockResolvedValue(mockBooking);
|
||||
|
||||
const result = await service.doesUserIdHaveAccessToBooking({
|
||||
userId: 123,
|
||||
bookingUid: "test-booking-uid",
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when user is not a host", async () => {
|
||||
const mockBooking = {
|
||||
userId: 456,
|
||||
user: { id: 456, email: "organizer@example.com" },
|
||||
eventType: {
|
||||
hosts: [{ userId: 789, user: { email: "host@example.com" } }],
|
||||
users: [],
|
||||
},
|
||||
attendees: [{ email: "host@example.com" }],
|
||||
};
|
||||
|
||||
mockBookingRepo.findByUidIncludeEventType.mockResolvedValue(mockBooking);
|
||||
mockUserRepo.getUserOrganizationAndTeams.mockResolvedValue(null);
|
||||
|
||||
const result = await service.doesUserIdHaveAccessToBooking({
|
||||
userId: 123,
|
||||
bookingUid: "test-booking-uid",
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Case 3: Team Event Access", () => {
|
||||
it("should return true when user has booking.readTeamBookings permission", async () => {
|
||||
const mockBooking = {
|
||||
userId: 456,
|
||||
eventType: {
|
||||
teamId: 100,
|
||||
},
|
||||
attendees: [],
|
||||
};
|
||||
|
||||
mockBookingRepo.findByUidIncludeEventType.mockResolvedValue(mockBooking);
|
||||
mockPermissionCheckService.checkPermission.mockResolvedValue(true);
|
||||
|
||||
const result = await service.doesUserIdHaveAccessToBooking({
|
||||
userId: 123,
|
||||
bookingUid: "test-booking-uid",
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockPermissionCheckService.checkPermission).toHaveBeenCalledWith({
|
||||
userId: 123,
|
||||
teamId: 100,
|
||||
permission: "booking.readTeamBookings",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false when user lacks booking.readTeamBookings permission", async () => {
|
||||
const mockBooking = {
|
||||
userId: 456,
|
||||
eventType: {
|
||||
teamId: 100,
|
||||
},
|
||||
attendees: [],
|
||||
};
|
||||
|
||||
mockBookingRepo.findByUidIncludeEventType.mockResolvedValue(mockBooking);
|
||||
mockPermissionCheckService.checkPermission.mockResolvedValue(false);
|
||||
|
||||
const result = await service.doesUserIdHaveAccessToBooking({
|
||||
userId: 123,
|
||||
bookingUid: "test-booking-uid",
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockPermissionCheckService.checkPermission).toHaveBeenCalledWith({
|
||||
userId: 123,
|
||||
teamId: 100,
|
||||
permission: "booking.readTeamBookings",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Case 4: Org Admin Access (Personal Bookings)", () => {
|
||||
it("should return true when user has booking.readOrgBookings permission", async () => {
|
||||
const mockBooking = {
|
||||
userId: 456,
|
||||
eventType: null,
|
||||
attendees: [],
|
||||
};
|
||||
|
||||
const mockBookingOwner = {
|
||||
organizationId: 200,
|
||||
teams: [],
|
||||
};
|
||||
|
||||
mockBookingRepo.findByUidIncludeEventType.mockResolvedValue(mockBooking);
|
||||
mockUserRepo.getUserOrganizationAndTeams.mockResolvedValue(mockBookingOwner);
|
||||
mockPermissionCheckService.checkPermission.mockResolvedValue(true);
|
||||
|
||||
const result = await service.doesUserIdHaveAccessToBooking({
|
||||
userId: 123,
|
||||
bookingUid: "test-booking-uid",
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockPermissionCheckService.checkPermission).toHaveBeenCalledWith({
|
||||
userId: 123,
|
||||
teamId: 200,
|
||||
permission: "booking.readOrgBookings",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false when user lacks booking.readOrgBookings permission", async () => {
|
||||
const mockBooking = {
|
||||
userId: 456,
|
||||
eventType: null,
|
||||
attendees: [],
|
||||
};
|
||||
|
||||
const mockBookingOwner = {
|
||||
organizationId: 200,
|
||||
teams: [],
|
||||
};
|
||||
|
||||
mockBookingRepo.findByUidIncludeEventType.mockResolvedValue(mockBooking);
|
||||
mockUserRepo.getUserOrganizationAndTeams.mockResolvedValue(mockBookingOwner);
|
||||
mockPermissionCheckService.checkPermission.mockResolvedValue(false);
|
||||
|
||||
const result = await service.doesUserIdHaveAccessToBooking({
|
||||
userId: 123,
|
||||
bookingUid: "test-booking-uid",
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Case 5: Team Admin Access (Personal Bookings)", () => {
|
||||
it("should return true when user has booking.readTeamBookings on ANY team", async () => {
|
||||
const mockBooking = {
|
||||
userId: 456,
|
||||
eventType: null,
|
||||
attendees: [],
|
||||
};
|
||||
|
||||
const mockBookingOwner = {
|
||||
organizationId: null,
|
||||
teams: [{ teamId: 300 }, { teamId: 400 }],
|
||||
};
|
||||
|
||||
mockBookingRepo.findByUidIncludeEventType.mockResolvedValue(mockBooking);
|
||||
mockUserRepo.getUserOrganizationAndTeams.mockResolvedValue(mockBookingOwner);
|
||||
mockPermissionCheckService.checkPermission
|
||||
.mockResolvedValueOnce(false) // Team 300 - no permission
|
||||
.mockResolvedValueOnce(true); // Team 400 - has permission
|
||||
|
||||
const result = await service.doesUserIdHaveAccessToBooking({
|
||||
userId: 123,
|
||||
bookingUid: "test-booking-uid",
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockPermissionCheckService.checkPermission).toHaveBeenCalledTimes(2);
|
||||
expect(mockPermissionCheckService.checkPermission).toHaveBeenNthCalledWith(1, {
|
||||
userId: 123,
|
||||
teamId: 300,
|
||||
permission: "booking.readTeamBookings",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
expect(mockPermissionCheckService.checkPermission).toHaveBeenNthCalledWith(2, {
|
||||
userId: 123,
|
||||
teamId: 400,
|
||||
permission: "booking.readTeamBookings",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false when user lacks permission on all teams", async () => {
|
||||
const mockBooking = {
|
||||
userId: 456,
|
||||
eventType: null,
|
||||
attendees: [],
|
||||
};
|
||||
|
||||
const mockBookingOwner = {
|
||||
organizationId: null,
|
||||
teams: [{ teamId: 300 }, { teamId: 400 }],
|
||||
};
|
||||
|
||||
mockBookingRepo.findByUidIncludeEventType.mockResolvedValue(mockBooking);
|
||||
mockUserRepo.getUserOrganizationAndTeams.mockResolvedValue(mockBookingOwner);
|
||||
mockPermissionCheckService.checkPermission.mockResolvedValue(false);
|
||||
|
||||
const result = await service.doesUserIdHaveAccessToBooking({
|
||||
userId: 123,
|
||||
bookingUid: "test-booking-uid",
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockPermissionCheckService.checkPermission).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,18 @@
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { BookingRepository } from "../repositories/BookingRepository";
|
||||
|
||||
type BookingForAccessCheck = NonNullable<Awaited<ReturnType<BookingRepository["findByUidIncludeEventType"]>>>;
|
||||
|
||||
export class BookingAccessService {
|
||||
constructor(private prismaClient: PrismaClient) {}
|
||||
private permissionCheckService: PermissionCheckService;
|
||||
|
||||
constructor(private prismaClient: PrismaClient) {
|
||||
this.permissionCheckService = new PermissionCheckService();
|
||||
}
|
||||
|
||||
private isUserAHost(userId: number, booking: BookingForAccessCheck): boolean {
|
||||
const hostMap = new Map<number, { id: number; email: string }>();
|
||||
@@ -38,9 +44,9 @@ export class BookingAccessService {
|
||||
* Determines if a user has access to a booking based on:
|
||||
* 1. Being the booking organizer
|
||||
* 2. Being one of the hosts in a multi-host booking
|
||||
* 3. Being a team/org admin where the event type belongs
|
||||
* 4. Being an org admin where the booking organizer belongs (for personal bookings)
|
||||
* 5. Being a team admin of any team the booking organizer belongs to (for personal bookings)
|
||||
* 3. Being a team/org admin where the event type belongs (uses PBAC if enabled)
|
||||
* 4. Being an org admin where the booking organizer belongs (uses PBAC if enabled, for personal bookings)
|
||||
* 5. Being a team admin of any team the booking organizer belongs to (uses PBAC if enabled, for personal bookings)
|
||||
*/
|
||||
async doesUserIdHaveAccessToBooking({
|
||||
userId,
|
||||
@@ -63,17 +69,23 @@ export class BookingAccessService {
|
||||
|
||||
if (!booking) return false;
|
||||
|
||||
// Case 1: User is the booking organizer
|
||||
if (userId === booking.userId) return true;
|
||||
|
||||
// Case 2: User is one of the hosts
|
||||
if (this.isUserAHost(userId, booking)) return true;
|
||||
|
||||
// If booking has a teamId, check if user is admin of that team/org
|
||||
// Case 3: If booking has a teamId, check if user has access to team bookings
|
||||
if (booking.eventType?.teamId) {
|
||||
const isAdminOrUser = await userRepo.isAdminOfTeamOrParentOrg({
|
||||
const teamId = booking.eventType.teamId;
|
||||
|
||||
const hasAccess = await this.permissionCheckService.checkPermission({
|
||||
userId,
|
||||
teamId: booking.eventType.teamId,
|
||||
teamId,
|
||||
permission: "booking.readTeamBookings",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
return isAdminOrUser;
|
||||
return hasAccess;
|
||||
}
|
||||
|
||||
// For managed events (child event types), check the parent's teamId
|
||||
@@ -91,22 +103,30 @@ export class BookingAccessService {
|
||||
|
||||
if (!bookingOwner) return false;
|
||||
|
||||
// Check if user is admin of booking organizer's organization
|
||||
// Case 4: Check if user is admin of booking organizer's organization
|
||||
if (bookingOwner.organizationId) {
|
||||
const isOrgAdmin = await userRepo.isAdminOfTeamOrParentOrg({
|
||||
const orgId = bookingOwner.organizationId;
|
||||
|
||||
const hasAccess = await this.permissionCheckService.checkPermission({
|
||||
userId,
|
||||
teamId: bookingOwner.organizationId,
|
||||
teamId: orgId,
|
||||
permission: "booking.readOrgBookings",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
if (isOrgAdmin) return true;
|
||||
if (hasAccess) return true;
|
||||
}
|
||||
|
||||
// Check if user is admin of any team the booking organizer belongs to
|
||||
// Case 5: Check if user is admin of any team the booking organizer belongs to
|
||||
for (const membership of bookingOwner.teams) {
|
||||
const isTeamAdmin = await userRepo.isAdminOfTeamOrParentOrg({
|
||||
const teamId = membership.teamId;
|
||||
|
||||
const hasAccess = await this.permissionCheckService.checkPermission({
|
||||
userId,
|
||||
teamId: membership.teamId,
|
||||
teamId,
|
||||
permission: "booking.readTeamBookings",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
if (isTeamAdmin) return true;
|
||||
if (hasAccess) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKey
|
||||
import { getVideoAdapters } from "@calcom/app-store/getVideoAdapters";
|
||||
import { sendBrokenIntegrationEmail } from "@calcom/emails/integration-email-service";
|
||||
import { getUid } from "@calcom/lib/CalEventParser";
|
||||
import { CAL_VIDEO, CAL_VIDEO_TYPE } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getPiiFreeCalendarEvent, getPiiFreeCredential } from "@calcom/lib/piiFreeData";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
@@ -167,8 +168,8 @@ const createMeetingWithCalVideo = async (calEvent: CalendarEvent) => {
|
||||
const [videoAdapter] = await getVideoAdapters([
|
||||
{
|
||||
id: 0,
|
||||
appId: "daily-video",
|
||||
type: "daily_video",
|
||||
appId: CAL_VIDEO,
|
||||
type: CAL_VIDEO_TYPE,
|
||||
userId: null,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
@@ -190,8 +191,8 @@ export const createInstantMeetingWithCalVideo = async (endTime: string) => {
|
||||
const [videoAdapter] = await getVideoAdapters([
|
||||
{
|
||||
id: 0,
|
||||
appId: "daily-video",
|
||||
type: "daily_video",
|
||||
appId: CAL_VIDEO,
|
||||
type: CAL_VIDEO_TYPE,
|
||||
userId: null,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
@@ -216,8 +217,8 @@ const getRecordingsOfCalVideoByRoomName = async (
|
||||
const [videoAdapter] = await getVideoAdapters([
|
||||
{
|
||||
id: 0,
|
||||
appId: "daily-video",
|
||||
type: "daily_video",
|
||||
appId: CAL_VIDEO,
|
||||
type: CAL_VIDEO_TYPE,
|
||||
userId: null,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
@@ -242,8 +243,8 @@ const getDownloadLinkOfCalVideoByRecordingId = async (
|
||||
const [videoAdapter] = await getVideoAdapters([
|
||||
{
|
||||
id: 0,
|
||||
appId: "daily-video",
|
||||
type: "daily_video",
|
||||
appId: CAL_VIDEO,
|
||||
type: CAL_VIDEO_TYPE,
|
||||
userId: null,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
@@ -266,8 +267,8 @@ const getAllTranscriptsAccessLinkFromRoomName = async (roomName: string) => {
|
||||
const [videoAdapter] = await getVideoAdapters([
|
||||
{
|
||||
id: 0,
|
||||
appId: "daily-video",
|
||||
type: "daily_video",
|
||||
appId: CAL_VIDEO,
|
||||
type: CAL_VIDEO_TYPE,
|
||||
userId: null,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
@@ -290,8 +291,8 @@ const getAllTranscriptsAccessLinkFromMeetingId = async (meetingId: string) => {
|
||||
const [videoAdapter] = await getVideoAdapters([
|
||||
{
|
||||
id: 0,
|
||||
appId: "daily-video",
|
||||
type: "daily_video",
|
||||
appId: CAL_VIDEO,
|
||||
type: CAL_VIDEO_TYPE,
|
||||
userId: null,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
@@ -314,8 +315,8 @@ const submitBatchProcessorTranscriptionJob = async (recordingId: string) => {
|
||||
const [videoAdapter] = await getVideoAdapters([
|
||||
{
|
||||
id: 0,
|
||||
appId: "daily-video",
|
||||
type: "daily_video",
|
||||
appId: CAL_VIDEO,
|
||||
type: CAL_VIDEO_TYPE,
|
||||
userId: null,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
@@ -350,8 +351,8 @@ const getTranscriptsAccessLinkFromRecordingId = async (recordingId: string) => {
|
||||
const [videoAdapter] = await getVideoAdapters([
|
||||
{
|
||||
id: 0,
|
||||
appId: "daily-video",
|
||||
type: "daily_video",
|
||||
appId: CAL_VIDEO,
|
||||
type: CAL_VIDEO_TYPE,
|
||||
userId: null,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
@@ -375,8 +376,8 @@ const checkIfRoomNameMatchesInRecording = async (roomName: string, recordingId:
|
||||
const [videoAdapter] = await getVideoAdapters([
|
||||
{
|
||||
id: 0,
|
||||
appId: "daily-video",
|
||||
type: "daily_video",
|
||||
appId: CAL_VIDEO,
|
||||
type: CAL_VIDEO_TYPE,
|
||||
userId: null,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
@@ -389,6 +390,31 @@ const checkIfRoomNameMatchesInRecording = async (roomName: string, recordingId:
|
||||
return videoAdapter?.checkIfRoomNameMatchesInRecording?.(roomName, recordingId);
|
||||
};
|
||||
|
||||
const getCalVideoMeetingSessionsByRoomName = async (roomName: string) => {
|
||||
let dailyAppKeys: Awaited<ReturnType<typeof getDailyAppKeys>>;
|
||||
try {
|
||||
dailyAppKeys = await getDailyAppKeys();
|
||||
} catch (e) {
|
||||
console.error("Error: Cal video provider is not installed.");
|
||||
return { data: [] };
|
||||
}
|
||||
const [videoAdapter] = await getVideoAdapters([
|
||||
{
|
||||
id: 0,
|
||||
appId: CAL_VIDEO,
|
||||
type: CAL_VIDEO_TYPE,
|
||||
userId: null,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
key: dailyAppKeys,
|
||||
invalid: false,
|
||||
delegationCredentialId: null,
|
||||
},
|
||||
]);
|
||||
|
||||
return videoAdapter?.getMeetingInformation?.(roomName) ?? { data: [] };
|
||||
};
|
||||
|
||||
export {
|
||||
getBusyVideoTimes,
|
||||
createMeeting,
|
||||
@@ -401,4 +427,5 @@ export {
|
||||
submitBatchProcessorTranscriptionJob,
|
||||
getTranscriptsAccessLinkFromRecordingId,
|
||||
checkIfRoomNameMatchesInRecording,
|
||||
getCalVideoMeetingSessionsByRoomName,
|
||||
};
|
||||
|
||||
@@ -248,6 +248,10 @@ export const RETELL_AI_TEST_EVENT_TYPE_MAP = (() => {
|
||||
export const ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS =
|
||||
process.env._CAL_INTERNAL_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS;
|
||||
|
||||
// Cal Video (Daily) app identifiers
|
||||
export const CAL_VIDEO = "daily-video";
|
||||
export const CAL_VIDEO_TYPE = "daily_video";
|
||||
|
||||
export const ORG_TRIAL_DAYS = process.env.STRIPE_ORG_TRIAL_DAYS
|
||||
? Math.max(0, parseInt(process.env.STRIPE_ORG_TRIAL_DAYS, 10))
|
||||
: null;
|
||||
|
||||
@@ -2,4 +2,7 @@ export {
|
||||
getRecordingsOfCalVideoByRoomName,
|
||||
getDownloadLinkOfCalVideoByRecordingId,
|
||||
getAllTranscriptsAccessLinkFromRoomName,
|
||||
getCalVideoMeetingSessionsByRoomName,
|
||||
} from "@calcom/features/conferencing/lib/videoClient";
|
||||
|
||||
export type { CalMeetingParticipant, CalMeetingSession } from "@calcom/app-store/dailyvideo/zod";
|
||||
|
||||
@@ -128,3 +128,5 @@ export { sendEmailVerificationByCode } from "@calcom/features/auth/lib/verifyEma
|
||||
export { checkEmailVerificationRequired } from "@calcom/trpc/server/routers/publicViewer/checkIfUserEmailVerificationRequired.handler";
|
||||
|
||||
export { TeamService } from "@calcom/features/ee/teams/services/teamService";
|
||||
|
||||
export { BookingAccessService } from "@calcom/features/bookings/services/BookingAccessService";
|
||||
|
||||
@@ -55,4 +55,12 @@ export class GetBookingRecordingsOutput {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => RecordingItem)
|
||||
data!: RecordingItem[];
|
||||
|
||||
@ApiProperty({
|
||||
example: "This endpoint will require authentication in a future release.",
|
||||
required: false,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
message?: string;
|
||||
}
|
||||
|
||||
@@ -14,4 +14,11 @@ export class GetBookingTranscriptsOutput {
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
data!: string[];
|
||||
|
||||
@ApiProperty({
|
||||
example: "This endpoint will require authentication in a future release.",
|
||||
required: false,
|
||||
})
|
||||
@IsString()
|
||||
message?: string;
|
||||
}
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested, IsNumber, IsString, IsBoolean, IsArray } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class CalMeetingParticipant {
|
||||
@ApiProperty({ example: "user123", nullable: true })
|
||||
@IsString()
|
||||
userId!: string | null;
|
||||
|
||||
@ApiProperty({ example: "John Doe", nullable: true })
|
||||
@IsString()
|
||||
userName!: string | null;
|
||||
|
||||
@ApiProperty({ example: 1678901234 })
|
||||
@IsNumber()
|
||||
joinTime!: number;
|
||||
|
||||
@ApiProperty({ example: 3600 })
|
||||
@IsNumber()
|
||||
duration!: number;
|
||||
}
|
||||
|
||||
export class CalMeetingSession {
|
||||
@ApiProperty({ example: "session123" })
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ example: "daily-video-room-123" })
|
||||
@IsString()
|
||||
room!: string;
|
||||
|
||||
@ApiProperty({ example: 1678901234 })
|
||||
@IsNumber()
|
||||
startTime!: number;
|
||||
|
||||
@ApiProperty({ example: 3600 })
|
||||
@IsNumber()
|
||||
duration!: number;
|
||||
|
||||
@ApiProperty({ example: false })
|
||||
@IsBoolean()
|
||||
ongoing!: boolean;
|
||||
|
||||
@ApiProperty({ example: 10 })
|
||||
@IsNumber()
|
||||
maxParticipants!: number;
|
||||
|
||||
@ApiProperty({ type: [CalMeetingParticipant] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CalMeetingParticipant)
|
||||
participants!: CalMeetingParticipant[];
|
||||
}
|
||||
|
||||
export class GetBookingVideoSessionsOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
error?: Error;
|
||||
|
||||
@ApiProperty({ type: [CalMeetingSession] })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CalMeetingSession)
|
||||
data!: CalMeetingSession[];
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export * from "./get-booking.output";
|
||||
export * from "./get-bookings.output";
|
||||
export * from "./get-booking-recordings.output";
|
||||
export * from "./get-booking-transcripts.output";
|
||||
export * from "./get-booking-video-sessions.output";
|
||||
|
||||
Vendored
+2
-1
@@ -2,6 +2,7 @@ import type {
|
||||
TSubmitBatchProcessorJobRes,
|
||||
batchProcessorBody,
|
||||
TGetTranscriptAccessLink,
|
||||
TGetMeetingInformationResponsesSchema,
|
||||
} from "@calcom/app-store/dailyvideo/zod";
|
||||
import type { GetRecordingsResponseSchema, GetAccessLinkResponseSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -44,7 +45,7 @@ export type VideoApiAdapter =
|
||||
|
||||
checkIfRoomNameMatchesInRecording?(roomName: string, recordingId: string): Promise<boolean>;
|
||||
|
||||
getMeetingInformation?(roomName: string): Promise<any>;
|
||||
getMeetingInformation?(roomName: string): Promise<TGetMeetingInformationResponsesSchema>;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user