Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a46375fed | ||
|
|
07de77ddc6 | ||
|
|
cf45443726 | ||
|
|
629b9391dd | ||
|
|
25da11c497 | ||
|
|
003e2630e7 | ||
|
|
4eef7138c1 | ||
|
|
253d0174be | ||
|
|
5ee5c117c4 | ||
|
|
0f268bbef7 | ||
|
|
d342e3f9e0 | ||
|
|
7f1e20fe19 | ||
|
|
3078423848 | ||
|
|
9953d73444 | ||
|
|
100136aae7 | ||
|
|
3c6e9aaa63 | ||
|
|
8fa39f2ae5 | ||
|
|
3aca72fb06 | ||
|
|
c46905e9a7 | ||
|
|
5ce96c7704 | ||
|
|
23eb77b8ac | ||
|
|
e63edc9d36 | ||
|
|
d33681eddd | ||
|
|
e7816cfb0c | ||
|
|
a268d8743d | ||
|
|
45db5c0d5e | ||
|
|
583661a15d | ||
|
|
461d5e0535 | ||
|
|
099d830e7e | ||
|
|
add59896f5 | ||
|
|
03b97fdb51 | ||
|
|
4c576fbd57 | ||
|
|
06ce1988f3 | ||
|
|
c059d79a51 | ||
|
|
b91c767d0c | ||
|
|
bb29edd30f | ||
|
|
b9a8b613bb | ||
|
|
76377d2d2c | ||
|
|
f4f09eaf9e |
@@ -19,6 +19,9 @@ export const isAdminGuard = async (req: NextApiRequest) => {
|
||||
accepted: true,
|
||||
team: {
|
||||
isOrganization: true,
|
||||
organizationSettings: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
},
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
},
|
||||
|
||||
@@ -278,8 +278,8 @@ async function postHandler(req: NextApiRequest) {
|
||||
|
||||
let data: Prisma.EventTypeCreateArgs["data"] = {
|
||||
...parsedBody,
|
||||
userId,
|
||||
users: { connect: { id: userId } },
|
||||
userId: !!parsedBody.teamId ? null : userId,
|
||||
users: !!parsedBody.teamId ? undefined : { connect: { id: userId } },
|
||||
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
|
||||
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
|
||||
};
|
||||
@@ -291,7 +291,7 @@ async function postHandler(req: NextApiRequest) {
|
||||
await checkUserMembership(req);
|
||||
}
|
||||
|
||||
if (isSystemWideAdmin && parsedBody.userId) {
|
||||
if (isSystemWideAdmin && parsedBody.userId && !parsedBody.teamId) {
|
||||
data = { ...parsedBody, users: { connect: { id: parsedBody.userId } } };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import prismaMock from "../../../../../../tests/libs/__mocks__/prismaMock";
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||
|
||||
import { buildEventType } from "@calcom/lib/test/builder";
|
||||
|
||||
import handler from "../../../pages/api/event-types/_post";
|
||||
import checkParentEventOwnership from "../../../pages/api/event-types/_utils/checkParentEventOwnership";
|
||||
import checkTeamEventEditPermission from "../../../pages/api/event-types/_utils/checkTeamEventEditPermission";
|
||||
import checkUserMembership from "../../../pages/api/event-types/_utils/checkUserMembership";
|
||||
import ensureOnlyMembersAsHosts from "../../../pages/api/event-types/_utils/ensureOnlyMembersAsHosts";
|
||||
import { canUserAccessTeamWithRole } from "../../../pages/api/teams/[teamId]/_auth-middleware";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
const adminUserId = 1;
|
||||
const memberUserId = 10;
|
||||
|
||||
vi.mock("../../../pages/api/teams/[teamId]/_auth-middleware", () => ({
|
||||
canUserAccessTeamWithRole: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../pages/api/event-types/_utils/checkUserMembership", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
vi.mock("../../../pages/api/event-types/_utils/checkParentEventOwnership", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
vi.mock("../../../pages/api/event-types/_utils/checkTeamEventEditPermission", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
vi.mock("../../../pages/api/event-types/_utils/ensureOnlyMembersAsHosts", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("POST /api/event-types", () => {
|
||||
describe("Errors", () => {
|
||||
test("should throw 401 if a non-admin user tries to create an event type with userId", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
title: "Tennis class",
|
||||
slug: "tennis-class-{{$guid}}",
|
||||
length: 60,
|
||||
hidden: true,
|
||||
userId: memberUserId,
|
||||
},
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(401);
|
||||
expect(JSON.parse(res._getData()).message).toBe("ADMIN required for `userId`");
|
||||
});
|
||||
test("should throw 401 if not system-wide admin and user cannot access teamId with required roles", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
title: "Tennis class",
|
||||
slug: "tennis-class-{{$guid}}",
|
||||
length: 60,
|
||||
hidden: true,
|
||||
teamId: 9999,
|
||||
},
|
||||
});
|
||||
req.userId = memberUserId;
|
||||
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false);
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(401);
|
||||
expect(JSON.parse(res._getData()).message).toBe("ADMIN required for `teamId`");
|
||||
});
|
||||
test("should throw 400 if system-wide admin but neither userId nor teamId provided", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
title: "Tennis class",
|
||||
slug: "tennis-class-{{$guid}}",
|
||||
length: 60,
|
||||
hidden: true,
|
||||
},
|
||||
});
|
||||
req.isSystemWideAdmin = true;
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(JSON.parse(res._getData()).message).toBe("`userId` or `teamId` required");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Success", () => {
|
||||
test("should call checkParentEventOwnership and checkUserMembership if parentId is present", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
title: "Tennis class",
|
||||
slug: "tennis-class-{{$guid}}",
|
||||
length: 60,
|
||||
hidden: true,
|
||||
userId: memberUserId,
|
||||
parentId: 9999,
|
||||
teamId: 9999,
|
||||
},
|
||||
});
|
||||
req.isSystemWideAdmin = true;
|
||||
req.userId = adminUserId;
|
||||
|
||||
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false);
|
||||
|
||||
await handler(req, res);
|
||||
expect(checkParentEventOwnership).toHaveBeenCalled();
|
||||
expect(checkUserMembership).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should create event type successfully if all conditions are met", async () => {
|
||||
const eventTypeTest = buildEventType();
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
title: "test title",
|
||||
slug: "test-slug",
|
||||
length: 60,
|
||||
hidden: true,
|
||||
userId: memberUserId,
|
||||
parentId: 9999,
|
||||
teamId: 9999,
|
||||
},
|
||||
});
|
||||
req.isSystemWideAdmin = true;
|
||||
req.userId = adminUserId;
|
||||
|
||||
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => true);
|
||||
vi.mocked(checkParentEventOwnership).mockImplementationOnce(async () => undefined);
|
||||
vi.mocked(checkUserMembership).mockImplementationOnce(async () => undefined);
|
||||
vi.mocked(checkTeamEventEditPermission).mockImplementationOnce(async () => undefined);
|
||||
vi.mocked(ensureOnlyMembersAsHosts).mockImplementationOnce(async () => undefined);
|
||||
|
||||
prismaMock.eventType.create.mockResolvedValue(eventTypeTest);
|
||||
|
||||
await handler(req, res);
|
||||
const data = JSON.parse(res._getData());
|
||||
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledTimes(1);
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(data.message).toBe("Event type created successfully");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -58,7 +58,7 @@ describe("isAdmin guard", () => {
|
||||
expect(scope).toBe(ScopeOfAdmin.SystemWide);
|
||||
});
|
||||
|
||||
it("Returns org-wide admin when user is set as such", async () => {
|
||||
it("Returns org-wide admin when user is set as such & admin API access is granted", async () => {
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {},
|
||||
@@ -67,10 +67,21 @@ describe("isAdmin guard", () => {
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
||||
|
||||
req.userId = adminUser.id;
|
||||
|
||||
const { isAdmin, scope } = await isAdminGuard(req);
|
||||
|
||||
expect(isAdmin).toBe(true);
|
||||
expect(scope).toBe(ScopeOfAdmin.OrgOwnerOrAdmin);
|
||||
});
|
||||
|
||||
it("Returns no admin when user is set as org admin but admin API access is revoked", async () => {
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {},
|
||||
});
|
||||
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-dunder@example.com" } });
|
||||
|
||||
req.userId = adminUser.id;
|
||||
const { isAdmin } = await isAdminGuard(req);
|
||||
expect(isAdmin).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://github.com/user-attachments/assets/ce7c2ecf-6097-469a-8512-c846a9fb665d" height="200" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
Cal.com is using the [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"dependencies": {
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-libraries-0.0.22": "npm:@calcom/platform-libraries@0.0.22",
|
||||
"@calcom/platform-libraries-0.0.23": "npm:@calcom/platform-libraries@0.0.23",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
||||
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
@@ -19,7 +20,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { handleNewBooking } from "@calcom/platform-libraries-0.0.22";
|
||||
import { handleNewBooking } from "@calcom/platform-libraries-0.0.23";
|
||||
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("Bookings Endpoints", () => {
|
||||
@@ -90,7 +91,10 @@ describe("Bookings Endpoints", () => {
|
||||
const bookingTimeZone = "Europe/London";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
const bookingMetadata = {};
|
||||
const bookingMetadata = {
|
||||
timeFormat: "12",
|
||||
meetingType: "organizer-phone",
|
||||
};
|
||||
const bookingResponses = {
|
||||
name: "tester",
|
||||
email: "tester@example.com",
|
||||
@@ -102,7 +106,7 @@ describe("Bookings Endpoints", () => {
|
||||
guests: [],
|
||||
};
|
||||
|
||||
const body = {
|
||||
const body: CreateBookingInput = {
|
||||
start: bookingStart,
|
||||
end: bookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
@@ -135,6 +139,64 @@ describe("Bookings Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("should reschedule a booking", () => {
|
||||
it("should reschedule with updated start time, end time & metadata", async () => {
|
||||
const newBookingStart = "2040-05-21T12:30:00.000Z";
|
||||
const newBookingEnd = "2040-05-21T13:30:00.000Z";
|
||||
const bookingEventTypeId = eventTypeId;
|
||||
const bookingTimeZone = "Europe/London";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
const newBookingMetadata = {
|
||||
timeFormat: "24",
|
||||
meetingType: "attendee-phone",
|
||||
};
|
||||
const bookingResponses = {
|
||||
name: "tester",
|
||||
email: "tester@example.com",
|
||||
location: {
|
||||
value: "link",
|
||||
optionValue: "",
|
||||
},
|
||||
notes: "test",
|
||||
guests: [],
|
||||
};
|
||||
|
||||
const body: CreateBookingInput = {
|
||||
rescheduleUid: createdBooking.uid,
|
||||
start: newBookingStart,
|
||||
end: newBookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: newBookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
responses: bookingResponses,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
|
||||
response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.userPrimaryEmail).toBeDefined();
|
||||
expect(responseBody.data.userPrimaryEmail).toEqual(userEmail);
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.uid).toBeDefined();
|
||||
expect(responseBody.data.startTime).toEqual(newBookingStart);
|
||||
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
|
||||
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
|
||||
expect(responseBody.data.metadata).toEqual(newBookingMetadata);
|
||||
|
||||
createdBooking = responseBody.data;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should get bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/v2/bookings?filters[status]=upcoming")
|
||||
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
getBookingInfo,
|
||||
handleCancelBooking,
|
||||
getBookingForReschedule,
|
||||
} from "@calcom/platform-libraries-0.0.22";
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
import type { AppsStatus } from "@calcom/platform-libraries-0.0.22";
|
||||
import type { AppsStatus } from "@calcom/platform-libraries-0.0.23";
|
||||
|
||||
export class CreateRecurringBookingInput extends CreateBookingInput {
|
||||
@IsBoolean()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { SUCCESS_STATUS, APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
|
||||
import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries-0.0.22";
|
||||
import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries-0.0.23";
|
||||
|
||||
@Injectable()
|
||||
export class AppleCalendarService implements CredentialSyncCalendarApp {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { User } from "@prisma/client";
|
||||
import { DateTime } from "luxon";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries-0.0.22";
|
||||
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries-0.0.23";
|
||||
import { Calendar } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ import {
|
||||
EventTypesPublic,
|
||||
eventTypeBookingFields,
|
||||
eventTypeLocations,
|
||||
} from "@calcom/platform-libraries-0.0.22";
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("Event types Endpoints", () => {
|
||||
|
||||
-1
@@ -40,7 +40,6 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
|
||||
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Controller({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { getEventTypeById } from "@calcom/platform-libraries-0.0.22";
|
||||
import { getEventTypeById } from "@calcom/platform-libraries-0.0.23";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import {
|
||||
updateEventType,
|
||||
EventTypesPublic,
|
||||
getEventTypesPublic,
|
||||
} from "@calcom/platform-libraries-0.0.22";
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getEventTypeById,
|
||||
transformApiEventTypeBookingFields,
|
||||
transformApiEventTypeLocations,
|
||||
} from "@calcom/platform-libraries-0.0.22";
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
|
||||
+3
-3
@@ -10,9 +10,9 @@ import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.22";
|
||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries-0.0.22";
|
||||
import { dynamicEvent } from "@calcom/platform-libraries-0.0.22";
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.23";
|
||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries-0.0.23";
|
||||
import { dynamicEvent } from "@calcom/platform-libraries-0.0.23";
|
||||
import {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { Injectable } from "@nestjs/common";
|
||||
import {
|
||||
transformApiEventTypeBookingFields,
|
||||
transformApiEventTypeLocations,
|
||||
} from "@calcom/platform-libraries-0.0.22";
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import {
|
||||
parseRecurringEvent,
|
||||
TransformedLocationsSchema,
|
||||
BookingFieldsSchema,
|
||||
} from "@calcom/platform-libraries-0.0.22";
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
|
||||
type EventTypeRelations = { users: User[]; schedule: Schedule | null };
|
||||
type DatabaseEventType = EventType & EventTypeRelations;
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { Injectable } from "@nestjs/common";
|
||||
import {
|
||||
transformApiScheduleOverrides,
|
||||
transformApiScheduleAvailability,
|
||||
} from "@calcom/platform-libraries-0.0.22";
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { ScheduleOverrideInput_2024_06_11 } from "@calcom/platform-types";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { TRPCError } from "@calcom/platform-libraries-0.0.22";
|
||||
import { TRPCError } from "@calcom/platform-libraries-0.0.23";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
|
||||
@Catch(TRPCError)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
type CachedData = {
|
||||
org?: Team;
|
||||
canAccess?: boolean;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class IsAdminAPIEnabledGuard implements CanActivate {
|
||||
constructor(
|
||||
private organizationsRepository: OrganizationsRepository,
|
||||
private readonly redisService: RedisService
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
let canAccess = false;
|
||||
const request = context.switchToHttp().getRequest<Request & { organization: Team }>();
|
||||
const organizationId: string = request.params.orgId;
|
||||
|
||||
if (!organizationId) {
|
||||
throw new ForbiddenException("No organization id found in request params.");
|
||||
}
|
||||
|
||||
const REDIS_CACHE_KEY = `apiv2:org:${organizationId}:guard:isAdminAccess`;
|
||||
const cachedData = await this.redisService.redis.get(REDIS_CACHE_KEY);
|
||||
|
||||
if (cachedData) {
|
||||
const { org: cachedOrg, canAccess: cachedCanAccess } = JSON.parse(cachedData) as CachedData;
|
||||
if (cachedOrg?.id === Number(organizationId) && cachedCanAccess !== undefined) {
|
||||
request.organization = cachedOrg;
|
||||
return cachedCanAccess;
|
||||
}
|
||||
}
|
||||
|
||||
const org = await this.organizationsRepository.findById(Number(organizationId));
|
||||
|
||||
if (org?.isOrganization && !org?.isPlatform) {
|
||||
const adminAPIAccessIsEnabledInOrg = await this.organizationsRepository.fetchOrgAdminApiStatus(
|
||||
Number(organizationId)
|
||||
);
|
||||
if (!adminAPIAccessIsEnabledInOrg) {
|
||||
throw new ForbiddenException(
|
||||
`Organization does not have Admin API access, please contact https://cal.com/sales to upgrade`
|
||||
);
|
||||
}
|
||||
}
|
||||
canAccess = true;
|
||||
org &&
|
||||
(await this.redisService.redis.set(
|
||||
REDIS_CACHE_KEY,
|
||||
JSON.stringify({ org: org, canAccess } satisfies CachedData),
|
||||
"EX",
|
||||
300
|
||||
));
|
||||
return canAccess;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { sendSignupToOrganizationEmail, getTranslation } from "@calcom/platform-libraries-0.0.22";
|
||||
import { sendSignupToOrganizationEmail, getTranslation } from "@calcom/platform-libraries-0.0.23";
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
|
||||
@@ -6,9 +6,17 @@ import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { UsersModule } from "./users/users.module";
|
||||
import { WebhooksModule } from "./webhooks/webhooks.module";
|
||||
|
||||
@Module({
|
||||
imports: [OAuthClientModule, BillingModule, PlatformEndpointsModule, TimezoneModule, UsersModule],
|
||||
imports: [
|
||||
OAuthClientModule,
|
||||
BillingModule,
|
||||
PlatformEndpointsModule,
|
||||
TimezoneModule,
|
||||
UsersModule,
|
||||
WebhooksModule,
|
||||
],
|
||||
})
|
||||
export class EndpointsModule implements NestModule {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import {
|
||||
EventTypeWebhookOutputResponseDto,
|
||||
EventTypeWebhooksOutputResponseDto,
|
||||
} from "@/modules/webhooks/outputs/event-type-webhook.output";
|
||||
import { DeleteManyWebhooksOutputResponseDto } from "@/modules/webhooks/outputs/webhook.output";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { EventType, Webhook } from "@calcom/prisma/client";
|
||||
|
||||
describe("EventTypes WebhooksController (e2e)", () => {
|
||||
let app: INestApplication;
|
||||
const userEmail = "event-types-webhook-controller-e2e@api.com";
|
||||
let user: UserWithProfile;
|
||||
let otherUser: UserWithProfile;
|
||||
let eventType: EventType;
|
||||
let eventType2: EventType;
|
||||
let otherEventType: EventType;
|
||||
|
||||
let eventTypeRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let webhookRepositoryFixture: WebhookRepositoryFixture;
|
||||
|
||||
let webhook: EventTypeWebhookOutputResponseDto["data"];
|
||||
let webhook2: Webhook;
|
||||
let otherWebhook: Webhook;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
webhookRepositoryFixture = new WebhookRepositoryFixture(moduleRef);
|
||||
eventTypeRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
|
||||
otherUser = await userRepositoryFixture.create({
|
||||
email: "other-user-webhook-controller@api.com",
|
||||
username: "other-user-webhook-controller@api.com",
|
||||
});
|
||||
|
||||
eventType = await eventTypeRepositoryFixture.create(
|
||||
{
|
||||
title: "Event Type 1",
|
||||
slug: "webhook-event-type-1",
|
||||
length: 60,
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
eventType2 = await eventTypeRepositoryFixture.create(
|
||||
{
|
||||
title: "Event Type 2",
|
||||
slug: "webhook-event-type-2",
|
||||
length: 60,
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
otherEventType = await eventTypeRepositoryFixture.create(
|
||||
{
|
||||
title: "Other Event Type ",
|
||||
slug: "other-webhook-event-type",
|
||||
length: 60,
|
||||
},
|
||||
otherUser.id
|
||||
);
|
||||
|
||||
otherWebhook = await webhookRepositoryFixture.create({
|
||||
id: "2mdfnn24",
|
||||
subscriberUrl: "https://example.com",
|
||||
eventTriggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
});
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
userRepositoryFixture.deleteByEmail(user.email);
|
||||
userRepositoryFixture.deleteByEmail(otherUser.email);
|
||||
webhookRepositoryFixture.delete(otherWebhook.id);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("/webhooks (POST)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/event-types/${eventType.id}/webhooks`)
|
||||
.send({
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
} satisfies CreateWebhookInputDto)
|
||||
.expect(201)
|
||||
.then(async (res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
eventTypeId: eventType.id,
|
||||
},
|
||||
} satisfies EventTypeWebhookOutputResponseDto);
|
||||
webhook = res.body.data;
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks (POST)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/event-types/${eventType2.id}/webhooks`)
|
||||
.send({
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
} satisfies CreateWebhookInputDto)
|
||||
.expect(201)
|
||||
.then(async (res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
eventTypeId: eventType2.id,
|
||||
},
|
||||
} satisfies EventTypeWebhookOutputResponseDto);
|
||||
webhook2 = res.body.data;
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks (POST) should fail to create a webhook for an event-type that does not belong to user", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/event-types/${otherEventType.id}/webhooks`)
|
||||
.send({
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
} satisfies CreateWebhookInputDto)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (PATCH)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/event-types/${eventType.id}/webhooks/${webhook.id}`)
|
||||
.send({
|
||||
active: false,
|
||||
} satisfies UpdateWebhookInputDto)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body.data.active).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (PATCH) should fail to patch a webhook for an event-type that does not belong to user", () => {
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/event-types/${otherEventType.id}/webhooks/${otherWebhook.id}`)
|
||||
.send({
|
||||
active: false,
|
||||
} satisfies UpdateWebhookInputDto)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/event-types/${eventType.id}/webhooks/${webhook.id}`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: false,
|
||||
payloadTemplate: "string",
|
||||
eventTypeId: eventType.id,
|
||||
},
|
||||
} satisfies EventTypeWebhookOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (GET) should fail to get a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer()).get(`/v2/event-types/${eventType.id}/webhooks/90284`).expect(404);
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (GET) should fail to get a webhook of an eventType that does not belong to user", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/event-types/${otherEventType.id}/webhooks/${otherWebhook.id}`)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (GET) should fail to get a webhook that does not belong to the eventType", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/event-types/${eventType.id}/webhooks/${otherWebhook.id}`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("/webhooks (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/event-types/${eventType.id}/webhooks`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
const responseBody = res.body as EventTypeWebhooksOutputResponseDto;
|
||||
responseBody.data.forEach((webhook) => {
|
||||
expect(webhook.eventTypeId).toBe(eventType.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/event-types/${eventType2.id}/webhooks`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
const responseBody = res.body as EventTypeWebhooksOutputResponseDto;
|
||||
responseBody.data.forEach((webhook) => {
|
||||
expect(webhook.eventTypeId).toBe(eventType2.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/event-types/${eventType.id}/webhooks/${webhook.id}`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: false,
|
||||
payloadTemplate: "string",
|
||||
eventTypeId: eventType.id,
|
||||
},
|
||||
} satisfies EventTypeWebhookOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks (DELETE)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/event-types/${eventType2.id}/webhooks`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: "1 webhooks deleted",
|
||||
} satisfies DeleteManyWebhooksOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/event-types/${eventType.id}/webhooks/1234453`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not belong to user", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/event-types/${otherEventType.id}/webhooks/${otherWebhook.id}`)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { GetWebhook } from "@/modules/webhooks/decorators/get-webhook-decorator";
|
||||
import { IsUserEventTypeWebhookGuard } from "@/modules/webhooks/guards/is-user-event-type-webhook-guard";
|
||||
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import {
|
||||
EventTypeWebhookOutputResponseDto,
|
||||
EventTypeWebhookOutputDto,
|
||||
EventTypeWebhooksOutputResponseDto,
|
||||
} from "@/modules/webhooks/outputs/event-type-webhook.output";
|
||||
import { DeleteManyWebhooksOutputResponseDto } from "@/modules/webhooks/outputs/webhook.output";
|
||||
import { PartialWebhookInputPipe, WebhookInputPipe } from "@/modules/webhooks/pipes/WebhookInputPipe";
|
||||
import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
|
||||
import { EventTypeWebhooksService } from "@/modules/webhooks/services/event-type-webhooks.service";
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
UseGuards,
|
||||
Get,
|
||||
Param,
|
||||
Query,
|
||||
Delete,
|
||||
Patch,
|
||||
ParseIntPipe,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { Webhook } from "@prisma/client";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/event-types/:eventTypeId/webhooks",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsUserEventTypeWebhookGuard)
|
||||
@ApiTags("Users' EventTypes Webhooks")
|
||||
export class EventTypeWebhooksController {
|
||||
constructor(
|
||||
private readonly webhooksService: WebhooksService,
|
||||
private readonly eventTypeWebhooksService: EventTypeWebhooksService
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@ApiOperation({ summary: "Create a webhook for an event-type" })
|
||||
async createEventTypeWebhook(
|
||||
@Body() body: CreateWebhookInputDto,
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
|
||||
): Promise<EventTypeWebhookOutputResponseDto> {
|
||||
const webhook = await this.eventTypeWebhooksService.createEventTypeWebhook(
|
||||
eventTypeId,
|
||||
new WebhookInputPipe().transform(body)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:webhookId")
|
||||
@ApiOperation({ summary: "Update a webhook of an event-type" })
|
||||
async updateEventTypeWebhook(
|
||||
@Body() body: UpdateWebhookInputDto,
|
||||
@Param("webhookId") webhookId: string
|
||||
): Promise<EventTypeWebhookOutputResponseDto> {
|
||||
const webhook = await this.webhooksService.updateWebhook(
|
||||
webhookId,
|
||||
new PartialWebhookInputPipe().transform(body)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:webhookId")
|
||||
@ApiOperation({ summary: "Get a webhook of an event-type" })
|
||||
async getEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise<EventTypeWebhookOutputResponseDto> {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@ApiOperation({ summary: "Get all webhooks of an event-type" })
|
||||
async getEventTypeWebhooks(
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Query() pagination: SkipTakePagination
|
||||
): Promise<EventTypeWebhooksOutputResponseDto> {
|
||||
const webhooks = await this.eventTypeWebhooksService.getEventTypeWebhooksPaginated(
|
||||
eventTypeId,
|
||||
pagination.skip ?? 0,
|
||||
pagination.take ?? 250
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: webhooks.map((webhook) =>
|
||||
plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:webhookId")
|
||||
@ApiOperation({ summary: "Delete a webhook of an event-type" })
|
||||
async deleteEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise<EventTypeWebhookOutputResponseDto> {
|
||||
await this.webhooksService.deleteWebhook(webhook.id);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/")
|
||||
@ApiOperation({ summary: "Delete all webhooks of an event-type" })
|
||||
async deleteAllEventTypeWebhooks(
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
|
||||
): Promise<DeleteManyWebhooksOutputResponseDto> {
|
||||
const data = await this.eventTypeWebhooksService.deleteAllEventTypeWebhooks(eventTypeId);
|
||||
return { status: SUCCESS_STATUS, data: `${data.count} webhooks deleted` };
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -15,6 +15,7 @@ import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
@@ -57,7 +58,7 @@ describe("OAuthFlow Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let usersRepositoryFixtures: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
@@ -83,7 +84,7 @@ describe("OAuthFlow Endpoints", () => {
|
||||
await app.init();
|
||||
|
||||
oAuthClientsRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
usersRepositoryFixtures = new UserRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries-0.0.22";
|
||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries-0.0.23";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientUsersService {
|
||||
|
||||
+7
-6
@@ -4,6 +4,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
@@ -49,7 +50,7 @@ export class OrganizationsEventTypesController {
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@Post("/teams/:teamId/event-types")
|
||||
async createTeamEventType(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@@ -72,7 +73,7 @@ export class OrganizationsEventTypesController {
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@Get("/teams/:teamId/event-types/:eventTypeId")
|
||||
async getTeamEventType(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@@ -90,7 +91,7 @@ export class OrganizationsEventTypesController {
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(IsOrgGuard, IsTeamInOrg)
|
||||
@UseGuards(IsOrgGuard, IsTeamInOrg, IsAdminAPIEnabledGuard)
|
||||
@Get("/teams/:teamId/event-types")
|
||||
async getTeamEventTypes(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@@ -116,7 +117,7 @@ export class OrganizationsEventTypesController {
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@Get("/teams/event-types")
|
||||
async getTeamsEventTypes(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@@ -133,7 +134,7 @@ export class OrganizationsEventTypesController {
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@Patch("/teams/:teamId/event-types/:eventTypeId")
|
||||
async updateTeamEventType(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@@ -156,7 +157,7 @@ export class OrganizationsEventTypesController {
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@Delete("/teams/:teamId/event-types/:eventTypeId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async deleteTeamEventType(
|
||||
|
||||
+3
-2
@@ -10,6 +10,7 @@ import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
@@ -30,7 +31,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
@@ -63,7 +64,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
+5
-4
@@ -11,6 +11,7 @@ import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.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 { withApiAuth } from "test/utils/withApiAuth";
|
||||
@@ -24,7 +25,7 @@ describe("Organizations Memberships Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let org: Team;
|
||||
@@ -51,7 +52,7 @@ describe("Organizations Memberships Endpoints", () => {
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
@@ -210,7 +211,7 @@ describe("Organizations Memberships Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let org: Team;
|
||||
@@ -228,7 +229,7 @@ describe("Organizations Memberships Endpoints", () => {
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsMembershipInOrg } from "@/modules/auth/guards/memberships/is-membership-in-org.guard";
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/inputs/create-organization-membership.input";
|
||||
@@ -41,7 +42,7 @@ import { Membership } from "@calcom/prisma/client";
|
||||
path: "/v2/organizations/:orgId/memberships",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@DocsTags("Organizations Memberships")
|
||||
export class OrganizationsMembershipsController {
|
||||
constructor(private organizationsMembershipService: OrganizationsMembershipService) {}
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.de
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
|
||||
@@ -40,7 +41,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
||||
path: "/v2/organizations/:orgId",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@DocsTags("Organizations Schedules")
|
||||
export class OrganizationsSchedulesController {
|
||||
constructor(
|
||||
|
||||
+5
-4
@@ -8,6 +8,7 @@ import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
@@ -31,7 +32,7 @@ describe("Organizations Schedules Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let membershipFixtures: MembershipRepositoryFixture;
|
||||
|
||||
const userEmail = "mr-robot@schedules-api.com";
|
||||
@@ -48,7 +49,7 @@ describe("Organizations Schedules Endpoints", () => {
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
@@ -110,7 +111,7 @@ describe("Organizations Schedules Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let membershipFixtures: MembershipRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
@@ -146,7 +147,7 @@ describe("Organizations Schedules Endpoints", () => {
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
|
||||
+4
-3
@@ -11,6 +11,7 @@ import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
@@ -25,7 +26,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
@@ -59,7 +60,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
@@ -335,7 +336,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
await userRepositoryFixture.deleteByEmail(nonOrgUser.email);
|
||||
await userRepositoryFixture.deleteByEmail(user2.email);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await organizationsRepositoryFixture.delete(nonOrgTeam.id);
|
||||
await teamsRepositoryFixture.delete(nonOrgTeam.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.de
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
@@ -40,7 +41,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
||||
path: "/v2/organizations/:orgId/teams/:teamId/memberships",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@DocsTags("Organizations Teams")
|
||||
export class OrganizationsTeamsMembershipsController {
|
||||
constructor(
|
||||
|
||||
+13
-6
@@ -12,6 +12,7 @@ import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.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 { withApiAuth } from "test/utils/withApiAuth";
|
||||
@@ -25,6 +26,7 @@ describe("Organizations Team Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
@@ -46,6 +48,7 @@ describe("Organizations Team Endpoints", () => {
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
@@ -54,7 +57,7 @@ describe("Organizations Team Endpoints", () => {
|
||||
username: userEmail,
|
||||
});
|
||||
|
||||
org = await teamsRepositoryFixture.create({
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: "Test Organization",
|
||||
isOrganization: true,
|
||||
});
|
||||
@@ -239,6 +242,7 @@ describe("Organizations Team Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
@@ -258,6 +262,7 @@ describe("Organizations Team Endpoints", () => {
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
@@ -266,7 +271,7 @@ describe("Organizations Team Endpoints", () => {
|
||||
username: userEmail,
|
||||
});
|
||||
|
||||
org = await teamsRepositoryFixture.create({
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: "Test Organization",
|
||||
isOrganization: true,
|
||||
});
|
||||
@@ -351,7 +356,7 @@ describe("Organizations Team Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
@@ -371,7 +376,7 @@ describe("Organizations Team Endpoints", () => {
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
@@ -478,6 +483,8 @@ describe("Organizations Team Endpoints", () => {
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let orgRepositoryFixture: OrganizationRepositoryFixture;
|
||||
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
|
||||
@@ -502,13 +509,13 @@ describe("Organizations Team Endpoints", () => {
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
|
||||
orgRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
|
||||
org = await teamsRepositoryFixture.create({
|
||||
org = await orgRepositoryFixture.create({
|
||||
name: "Platform Test Organization",
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
@@ -43,7 +44,7 @@ import { Team } from "@calcom/prisma/client";
|
||||
path: "/v2/organizations/:orgId/teams",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@DocsTags("Organizations Teams")
|
||||
export class OrganizationsTeamsController {
|
||||
constructor(private organizationsTeamsService: OrganizationsTeamsService) {}
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
|
||||
@@ -41,7 +42,7 @@ import { Team } from "@calcom/prisma/client";
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@UseGuards(IsOrgGuard)
|
||||
@DocsTags("Organizations Users")
|
||||
export class OrganizationsUsersController {
|
||||
|
||||
+11
-7
@@ -10,6 +10,7 @@ import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
@@ -23,7 +24,7 @@ describe("Organizations Users Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let membershipFixtures: MembershipRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
@@ -40,7 +41,7 @@ describe("Organizations Users Endpoints", () => {
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
@@ -113,7 +114,7 @@ describe("Organizations Users Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let membershipFixtures: MembershipRepositoryFixture;
|
||||
|
||||
const userEmail = "admin1@org.com";
|
||||
@@ -148,7 +149,7 @@ describe("Organizations Users Endpoints", () => {
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
@@ -349,7 +350,8 @@ describe("Organizations Users Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let membershipFixtures: MembershipRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
@@ -370,7 +372,9 @@ describe("Organizations Users Endpoints", () => {
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
@@ -380,7 +384,7 @@ describe("Organizations Users Endpoints", () => {
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
team = await organizationsRepositoryFixture.create({
|
||||
team = await teamsRepositoryFixture.create({
|
||||
name: "Test org 4 team",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: org.id } },
|
||||
|
||||
@@ -102,4 +102,15 @@ export class OrganizationsRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchOrgAdminApiStatus(organizationId: number) {
|
||||
return this.dbRead.prisma.organizationSettings.findUnique({
|
||||
where: {
|
||||
organizationId,
|
||||
},
|
||||
select: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.22";
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.23";
|
||||
import {
|
||||
CreateTeamEventTypeInput_2024_06_14,
|
||||
UpdateTeamEventTypeInput_2024_06_14,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/repositori
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries-0.0.22";
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries-0.0.23";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsTeamsService {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
|
||||
import { Injectable, ConflictException } from "@nestjs/common";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries-0.0.22";
|
||||
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries-0.0.23";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { OnModuleInit } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { OnModuleDestroy, OnModuleInit } from "@nestjs/common";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class PrismaReadService implements OnModuleInit {
|
||||
export class PrismaReadService implements OnModuleInit, OnModuleDestroy {
|
||||
private logger = new Logger("PrismaReadService");
|
||||
|
||||
public prisma: PrismaClient;
|
||||
|
||||
constructor(readonly configService: ConfigService) {
|
||||
@@ -22,4 +24,12 @@ export class PrismaReadService implements OnModuleInit {
|
||||
async onModuleInit() {
|
||||
this.prisma.$connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
try {
|
||||
await this.prisma.$disconnect();
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { OnModuleInit } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { OnModuleDestroy, OnModuleInit } from "@nestjs/common";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class PrismaWriteService implements OnModuleInit {
|
||||
export class PrismaWriteService implements OnModuleInit, OnModuleDestroy {
|
||||
private logger = new Logger("PrismaWriteService");
|
||||
|
||||
public prisma: PrismaClient;
|
||||
|
||||
constructor(readonly configService: ConfigService) {
|
||||
@@ -22,4 +24,12 @@ export class PrismaWriteService implements OnModuleInit {
|
||||
async onModuleInit() {
|
||||
this.prisma.$connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
try {
|
||||
await this.prisma.$disconnect();
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries-0.0.22";
|
||||
import type { AvailableSlotsType } from "@calcom/platform-libraries-0.0.22";
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries-0.0.23";
|
||||
import type { AvailableSlotsType } from "@calcom/platform-libraries-0.0.23";
|
||||
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
|
||||
import { ApiResponse, GetAvailableSlotsInput } from "@calcom/platform-types";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries-0.0.22";
|
||||
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries-0.0.23";
|
||||
import { ReserveSlotInput } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Controller, Get } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries-0.0.22";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries-0.0.23";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { cityTimezonesHandler } from "@calcom/platform-libraries-0.0.22";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries-0.0.22";
|
||||
import { cityTimezonesHandler } from "@calcom/platform-libraries-0.0.23";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries-0.0.23";
|
||||
|
||||
@Injectable()
|
||||
export class TimezonesService {
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import {
|
||||
UserWebhookOutputResponseDto,
|
||||
UserWebhooksOutputResponseDto,
|
||||
} from "@/modules/webhooks/outputs/user-webhook.output";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { Webhook } from "@calcom/prisma/client";
|
||||
|
||||
describe("WebhooksController (e2e)", () => {
|
||||
let app: INestApplication;
|
||||
const userEmail = "webhook-controller-e2e@api.com";
|
||||
let user: UserWithProfile;
|
||||
let otherUser: UserWithProfile;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let webhookRepositoryFixture: WebhookRepositoryFixture;
|
||||
|
||||
let webhook: UserWebhookOutputResponseDto["data"];
|
||||
let otherWebhook: Webhook;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
webhookRepositoryFixture = new WebhookRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
|
||||
otherUser = await userRepositoryFixture.create({
|
||||
email: "other-user-webhook-controller@api.com",
|
||||
username: "other-user-webhook-controller@api.com",
|
||||
});
|
||||
|
||||
otherWebhook = await webhookRepositoryFixture.create({
|
||||
id: "2mdfnn2",
|
||||
subscriberUrl: "https://example.com",
|
||||
eventTriggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
});
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
userRepositoryFixture.deleteByEmail(user.email);
|
||||
userRepositoryFixture.deleteByEmail(otherUser.email);
|
||||
webhookRepositoryFixture.delete(otherWebhook.id);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("/webhooks (POST)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/webhooks")
|
||||
.send({
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
} satisfies CreateWebhookInputDto)
|
||||
.expect(201)
|
||||
.then(async (res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
userId: user.id,
|
||||
},
|
||||
} satisfies UserWebhookOutputResponseDto);
|
||||
webhook = res.body.data;
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks (POST) should fail to create a webhook that already has same userId / subcriberUrl combo", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/webhooks")
|
||||
.send({
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
} satisfies CreateWebhookInputDto)
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (PATCH)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/webhooks/${webhook.id}`)
|
||||
.send({
|
||||
active: false,
|
||||
} satisfies UpdateWebhookInputDto)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body.data.active).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/webhooks/${webhook.id}`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: false,
|
||||
payloadTemplate: "string",
|
||||
userId: user.id,
|
||||
},
|
||||
} satisfies UserWebhookOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (GET) should fail to get a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer()).get(`/v2/webhooks/90284`).expect(404);
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (GET) should fail to get a webhook that does not belong to user", () => {
|
||||
return request(app.getHttpServer()).get(`/v2/webhooks/${otherWebhook.id}`).expect(403);
|
||||
});
|
||||
|
||||
it("/webhooks (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/v2/webhooks")
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
const responseBody = res.body as UserWebhooksOutputResponseDto;
|
||||
responseBody.data.forEach((webhook) => {
|
||||
expect(webhook.userId).toBe(user.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (DELETE)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/webhooks/${webhook.id}`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: false,
|
||||
payloadTemplate: "string",
|
||||
userId: user.id,
|
||||
},
|
||||
} satisfies UserWebhookOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer()).delete(`/v2/webhooks/12993`).expect(404);
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not belong to user", () => {
|
||||
return request(app.getHttpServer()).delete(`/v2/webhooks/${otherWebhook.id}`).expect(403);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { GetWebhook } from "@/modules/webhooks/decorators/get-webhook-decorator";
|
||||
import { IsUserWebhookGuard } from "@/modules/webhooks/guards/is-user-webhook-guard";
|
||||
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import {
|
||||
UserWebhookOutputDto,
|
||||
UserWebhookOutputResponseDto,
|
||||
UserWebhooksOutputResponseDto,
|
||||
} from "@/modules/webhooks/outputs/user-webhook.output";
|
||||
import { PartialWebhookInputPipe, WebhookInputPipe } from "@/modules/webhooks/pipes/WebhookInputPipe";
|
||||
import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
|
||||
import { UserWebhooksService } from "@/modules/webhooks/services/user-webhooks.service";
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import { Controller, Post, Body, UseGuards, Get, Param, Query, Delete, Patch } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { Webhook } from "@prisma/client";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/webhooks",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiTags("Users' Webhooks")
|
||||
export class WebhooksController {
|
||||
constructor(
|
||||
private readonly webhooksService: WebhooksService,
|
||||
private readonly userWebhooksService: UserWebhooksService
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@ApiOperation({ summary: "Create a webhook" })
|
||||
async createWebhook(
|
||||
@Body() body: CreateWebhookInputDto,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<UserWebhookOutputResponseDto> {
|
||||
const webhook = await this.userWebhooksService.createUserWebhook(
|
||||
user.id,
|
||||
new WebhookInputPipe().transform(body)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(UserWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:webhookId")
|
||||
@ApiOperation({ summary: "Update a webhook" })
|
||||
@UseGuards(IsUserWebhookGuard)
|
||||
async updateWebhook(
|
||||
@Param("webhookId") webhookId: string,
|
||||
@Body() body: UpdateWebhookInputDto
|
||||
): Promise<UserWebhookOutputResponseDto> {
|
||||
const webhook = await this.webhooksService.updateWebhook(
|
||||
webhookId,
|
||||
new PartialWebhookInputPipe().transform(body)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(UserWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:webhookId")
|
||||
@ApiOperation({ summary: "Get a webhook" })
|
||||
@UseGuards(IsUserWebhookGuard)
|
||||
async getWebhook(@GetWebhook() webhook: Webhook): Promise<UserWebhookOutputResponseDto> {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(UserWebhookOutputDto, new WebhookOutputPipe().transform(webhook)),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@ApiOperation({ summary: "Get all user webhooks paginated" })
|
||||
async getWebhooks(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Query() query: SkipTakePagination
|
||||
): Promise<UserWebhooksOutputResponseDto> {
|
||||
const webhooks = await this.userWebhooksService.getUserWebhooksPaginated(
|
||||
user.id,
|
||||
query.skip ?? 0,
|
||||
query.take ?? 250
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: webhooks.map((webhook) =>
|
||||
plainToClass(UserWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:webhookId")
|
||||
@ApiOperation({ summary: "Delete a webhook" })
|
||||
@UseGuards(IsUserWebhookGuard)
|
||||
async deleteWebhook(@Param("webhookId") webhookId: string): Promise<UserWebhookOutputResponseDto> {
|
||||
const webhook = await this.webhooksService.deleteWebhook(webhookId);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(UserWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
import { createParamDecorator } from "@nestjs/common";
|
||||
|
||||
import { Webhook } from "@calcom/prisma/client";
|
||||
|
||||
export type GetWebhookReturnType = Webhook;
|
||||
|
||||
export const GetWebhook = createParamDecorator<
|
||||
keyof GetWebhookReturnType | (keyof GetWebhookReturnType)[],
|
||||
ExecutionContext
|
||||
>((data, ctx) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const webhook = request.webhook as GetWebhookReturnType;
|
||||
|
||||
if (!webhook) {
|
||||
throw new Error("GetWebhook decorator : Webhook not found");
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data.reduce((prev, curr) => {
|
||||
return {
|
||||
...prev,
|
||||
[curr]: webhook[curr],
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (data) {
|
||||
return webhook[data];
|
||||
}
|
||||
|
||||
return webhook;
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import {
|
||||
BadRequestException,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { EventType, Webhook } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class IsUserEventTypeWebhookGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly webhooksService: WebhooksService,
|
||||
private readonly eventtypesRepository: EventTypesRepository_2024_06_14
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context
|
||||
.switchToHttp()
|
||||
.getRequest<Request & { webhook: Webhook } & { eventType: EventType }>();
|
||||
const user = request.user as GetUserReturnType;
|
||||
const webhookId = request.params.webhookId;
|
||||
const eventTypeId = request.params.eventTypeId;
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (eventTypeId) {
|
||||
const eventType = await this.eventtypesRepository.getEventTypeById(parseInt(eventTypeId));
|
||||
if (!eventType) {
|
||||
throw new NotFoundException(`Event type (${eventTypeId}) not found`);
|
||||
}
|
||||
if (eventType.userId !== user.id) {
|
||||
throw new ForbiddenException(`User (${user.id}) is not the owner of event type (${eventTypeId})`);
|
||||
}
|
||||
request.eventType = eventType;
|
||||
}
|
||||
|
||||
if (webhookId) {
|
||||
const webhook = await this.webhooksService.getWebhookById(webhookId);
|
||||
if (!webhook.eventTypeId) {
|
||||
throw new BadRequestException(`Webhook (${webhookId}) is not associated with an event type`);
|
||||
}
|
||||
if (webhook.eventTypeId !== parseInt(eventTypeId)) {
|
||||
throw new ForbiddenException(
|
||||
`Webhook (${webhookId}) is not associated with event type (${eventTypeId})`
|
||||
);
|
||||
}
|
||||
request.webhook = webhook;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { Webhook } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class IsUserWebhookGuard implements CanActivate {
|
||||
constructor(private readonly webhooksService: WebhooksService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { webhook: Webhook }>();
|
||||
const user = request.user as GetUserReturnType;
|
||||
const webhookId = request.params.webhookId;
|
||||
|
||||
if (!user || !webhookId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const webhook = await this.webhooksService.getWebhookById(webhookId);
|
||||
|
||||
if (webhook.userId !== user.id) {
|
||||
return user.isSystemAdmin;
|
||||
}
|
||||
|
||||
request.webhook = webhook;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ApiProperty, PartialType } from "@nestjs/swagger";
|
||||
import { WebhookTriggerEvents } from "@prisma/client";
|
||||
import { IsArray, IsBoolean, IsEnum, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class CreateWebhookInputDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description:
|
||||
"The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
example: JSON.stringify({
|
||||
content: "A new event has been scheduled",
|
||||
type: "{{type}}",
|
||||
name: "{{title}}",
|
||||
organizer: "{{organizer.name}}",
|
||||
booker: "{{attendees.0.name}}",
|
||||
}),
|
||||
})
|
||||
payloadTemplate?: string;
|
||||
|
||||
@IsBoolean()
|
||||
active!: boolean;
|
||||
|
||||
@IsString()
|
||||
subscriberUrl!: string;
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty({
|
||||
example: [
|
||||
"BOOKING_CREATED",
|
||||
"BOOKING_RESCHEDULED",
|
||||
"BOOKING_CANCELLED",
|
||||
"BOOKING_CONFIRMED",
|
||||
"BOOKING_REJECTED",
|
||||
"BOOKING_COMPLETED",
|
||||
"BOOKING_NO_SHOW",
|
||||
"BOOKING_REOPENED",
|
||||
],
|
||||
enum: WebhookTriggerEvents,
|
||||
})
|
||||
@IsEnum(WebhookTriggerEvents, { each: true })
|
||||
triggers!: WebhookTriggerEvents[];
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
secret?: string;
|
||||
}
|
||||
|
||||
export class UpdateWebhookInputDto extends PartialType(CreateWebhookInputDto) {}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsInt, IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
import { WebhookOutputDto } from "./webhook.output";
|
||||
|
||||
export class EventTypeWebhookOutputDto extends WebhookOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly eventTypeId!: number;
|
||||
}
|
||||
|
||||
export class EventTypeWebhookOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => WebhookOutputDto)
|
||||
data!: EventTypeWebhookOutputDto;
|
||||
}
|
||||
|
||||
export class EventTypeWebhooksOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => WebhookOutputDto)
|
||||
data!: EventTypeWebhookOutputDto[];
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsInt, IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
import { WebhookOutputDto } from "./webhook.output";
|
||||
|
||||
export class UserWebhookOutputDto extends WebhookOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly userId!: number;
|
||||
}
|
||||
|
||||
export class UserWebhookOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => WebhookOutputDto)
|
||||
data!: UserWebhookOutputDto;
|
||||
}
|
||||
|
||||
export class UserWebhooksOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => WebhookOutputDto)
|
||||
data!: UserWebhookOutputDto[];
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { WebhookTriggerEvents } from "@prisma/client";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsBoolean, IsEnum, IsInt, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class WebhookOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly id!: number;
|
||||
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiProperty({
|
||||
description:
|
||||
"The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
example: JSON.stringify({
|
||||
content: "A new event has been scheduled",
|
||||
type: "{{type}}",
|
||||
name: "{{title}}",
|
||||
organizer: "{{organizer.name}}",
|
||||
booker: "{{attendees.0.name}}",
|
||||
}),
|
||||
})
|
||||
readonly payloadTemplate!: string;
|
||||
|
||||
@IsEnum(WebhookTriggerEvents)
|
||||
@Expose()
|
||||
readonly triggers!: WebhookTriggerEvents[];
|
||||
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly subscriberUrl!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly active!: boolean;
|
||||
}
|
||||
|
||||
export class DeleteManyWebhooksOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
@Expose()
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => WebhookOutputDto)
|
||||
data!: string;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import { PipeTransform, Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class WebhookInputPipe implements PipeTransform {
|
||||
transform(value: CreateWebhookInputDto) {
|
||||
const { triggers, ...rest } = value;
|
||||
const eventTriggers = triggers;
|
||||
const parsedData = { ...rest, eventTriggers };
|
||||
return parsedData;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PartialWebhookInputPipe implements PipeTransform {
|
||||
transform(value: UpdateWebhookInputDto) {
|
||||
if (value.triggers) {
|
||||
const { triggers, ...rest } = value;
|
||||
const eventTriggers = triggers;
|
||||
const parsedData = { ...rest, eventTriggers };
|
||||
return parsedData;
|
||||
}
|
||||
return { ...value, eventTriggers: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
export type PipedInputWebhookType = ReturnType<WebhookInputPipe["transform"]>;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { PipeTransform, Injectable } from "@nestjs/common";
|
||||
import { Webhook } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class WebhookOutputPipe implements PipeTransform {
|
||||
transform(value: Webhook) {
|
||||
if (value?.eventTriggers) {
|
||||
const { eventTriggers, ...rest } = value;
|
||||
const triggers = eventTriggers;
|
||||
const parsedData = { ...rest, triggers };
|
||||
return parsedData;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export type PipedOutputWebhookType = ReturnType<WebhookOutputPipe["transform"]>;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { PipedInputWebhookType } from "@/modules/webhooks/pipes/WebhookInputPipe";
|
||||
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
|
||||
import { ConflictException, Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class EventTypeWebhooksService {
|
||||
constructor(private readonly webhooksRepository: WebhooksRepository) {}
|
||||
|
||||
async createEventTypeWebhook(eventTypeId: number, body: PipedInputWebhookType) {
|
||||
const existingWebhook = await this.webhooksRepository.getEventTypeWebhookByUrl(
|
||||
eventTypeId,
|
||||
body.subscriberUrl
|
||||
);
|
||||
if (existingWebhook) {
|
||||
throw new ConflictException("Webhook with this subscriber url already exists for this event type");
|
||||
}
|
||||
return this.webhooksRepository.createEventTypeWebhook(eventTypeId, {
|
||||
...body,
|
||||
payloadTemplate: body.payloadTemplate ?? null,
|
||||
secret: body.secret ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
getEventTypeWebhooksPaginated(eventTypeId: number, skip: number, take: number) {
|
||||
return this.webhooksRepository.getEventTypeWebhooksPaginated(eventTypeId, skip, take);
|
||||
}
|
||||
|
||||
async deleteAllEventTypeWebhooks(eventTypeId: number): Promise<{ count: number }> {
|
||||
return this.webhooksRepository.deleteAllEventTypeWebhooks(eventTypeId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { PipedInputWebhookType } from "@/modules/webhooks/pipes/WebhookInputPipe";
|
||||
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
|
||||
import { ConflictException, Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class UserWebhooksService {
|
||||
constructor(private readonly webhooksRepository: WebhooksRepository) {}
|
||||
|
||||
async createUserWebhook(userId: number, body: PipedInputWebhookType) {
|
||||
const existingWebhook = await this.webhooksRepository.getUserWebhookByUrl(userId, body.subscriberUrl);
|
||||
if (existingWebhook) {
|
||||
throw new ConflictException("Webhook with this subscriber url already exists for this user");
|
||||
}
|
||||
|
||||
return this.webhooksRepository.createUserWebhook(userId, {
|
||||
...body,
|
||||
payloadTemplate: body.payloadTemplate ?? null,
|
||||
secret: body.secret ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async getUserWebhooksPaginated(userId: number, skip: number, take: number) {
|
||||
return this.webhooksRepository.getUserWebhooksPaginated(userId, skip, take);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class WebhooksService {
|
||||
constructor(private readonly webhooksRepository: WebhooksRepository) {}
|
||||
|
||||
async updateWebhook(webhookId: string, body: UpdateWebhookInputDto) {
|
||||
return this.webhooksRepository.updateWebhook(webhookId, body);
|
||||
}
|
||||
|
||||
async getWebhookById(webhookId: string) {
|
||||
const webhook = await this.webhooksRepository.getWebhookById(webhookId);
|
||||
if (!webhook) {
|
||||
throw new NotFoundException(`Webhook (${webhookId}) not found`);
|
||||
}
|
||||
return webhook;
|
||||
}
|
||||
|
||||
async deleteWebhook(webhookId: string) {
|
||||
return this.webhooksRepository.deleteWebhook(webhookId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { EventTypeWebhooksController } from "@/modules/event-types/controllers/event-types-webhooks.controller";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { PrismaModule } from "../prisma/prisma.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { WebhooksController } from "./controllers/webhooks.controller";
|
||||
import { EventTypeWebhooksService } from "./services/event-type-webhooks.service";
|
||||
import { UserWebhooksService } from "./services/user-webhooks.service";
|
||||
import { WebhooksService } from "./services/webhooks.service";
|
||||
import { WebhooksRepository } from "./webhooks.repository";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, UsersModule, EventTypesModule_2024_06_14],
|
||||
controllers: [WebhooksController, EventTypeWebhooksController],
|
||||
providers: [WebhooksService, WebhooksRepository, UserWebhooksService, EventTypeWebhooksService],
|
||||
exports: [WebhooksService, WebhooksRepository, UserWebhooksService, EventTypeWebhooksService],
|
||||
})
|
||||
export class WebhooksModule {}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { Webhook } from "@calcom/prisma/client";
|
||||
|
||||
import { PrismaWriteService } from "../prisma/prisma-write.service";
|
||||
|
||||
type WebhookInputData = Pick<
|
||||
Webhook,
|
||||
"payloadTemplate" | "eventTriggers" | "subscriberUrl" | "secret" | "active"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
export class WebhooksRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async createUserWebhook(userId: number, data: WebhookInputData) {
|
||||
const id = uuidv4();
|
||||
return this.dbWrite.prisma.webhook.create({
|
||||
data: { ...data, id, userId },
|
||||
});
|
||||
}
|
||||
|
||||
async createEventTypeWebhook(eventTypeId: number, data: WebhookInputData) {
|
||||
const id = uuidv4();
|
||||
return this.dbWrite.prisma.webhook.create({
|
||||
data: { ...data, id, eventTypeId },
|
||||
});
|
||||
}
|
||||
|
||||
async updateWebhook(webhookId: string, data: Partial<WebhookInputData>) {
|
||||
return this.dbWrite.prisma.webhook.update({
|
||||
where: { id: webhookId },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async getWebhookById(webhookId: string) {
|
||||
return this.dbRead.prisma.webhook.findFirst({
|
||||
where: { id: webhookId },
|
||||
});
|
||||
}
|
||||
|
||||
async getUserWebhooksPaginated(userId: number, skip: number, take: number) {
|
||||
return this.dbRead.prisma.webhook.findMany({
|
||||
where: { userId },
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypeWebhooksPaginated(eventTypeId: number, skip: number, take: number) {
|
||||
return this.dbRead.prisma.webhook.findMany({
|
||||
where: { eventTypeId },
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
}
|
||||
|
||||
async getUserWebhookByUrl(userId: number, subscriberUrl: string) {
|
||||
return this.dbRead.prisma.webhook.findFirst({
|
||||
where: { userId, subscriberUrl },
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypeWebhookByUrl(eventTypeId: number, subscriberUrl: string) {
|
||||
return this.dbRead.prisma.webhook.findFirst({
|
||||
where: { eventTypeId, subscriberUrl },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteWebhook(webhookId: string) {
|
||||
return this.dbWrite.prisma.webhook.delete({
|
||||
where: { id: webhookId },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAllEventTypeWebhooks(eventTypeId: number) {
|
||||
return this.dbWrite.prisma.webhook.deleteMany({
|
||||
where: { eventTypeId },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1219,16 +1219,6 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateScheduleInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
@@ -1331,16 +1321,6 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateScheduleInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
@@ -2380,16 +2360,6 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateScheduleInput_2024_04_15"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
@@ -3261,6 +3231,369 @@
|
||||
"Timezones"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/webhooks": {
|
||||
"post": {
|
||||
"operationId": "WebhooksController_createWebhook",
|
||||
"summary": "Create a webhook",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateWebhookInputDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' Webhooks"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "WebhooksController_getWebhooks",
|
||||
"summary": "Get all user webhooks paginated",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to return",
|
||||
"example": 10,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to skip",
|
||||
"example": 0,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserWebhooksOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' Webhooks"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/webhooks/{webhookId}": {
|
||||
"patch": {
|
||||
"operationId": "WebhooksController_updateWebhook",
|
||||
"summary": "Update a webhook",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "webhookId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateWebhookInputDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' Webhooks"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "WebhooksController_getWebhook",
|
||||
"summary": "Get a webhook",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' Webhooks"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "WebhooksController_deleteWebhook",
|
||||
"summary": "Delete a webhook",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "webhookId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' Webhooks"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/event-types/{eventTypeId}/webhooks": {
|
||||
"post": {
|
||||
"operationId": "EventTypeWebhooksController_createEventTypeWebhook",
|
||||
"summary": "Create a webhook for an event-type",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "eventTypeId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateWebhookInputDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "EventTypeWebhooksController_getEventTypeWebhooks",
|
||||
"summary": "Get all webhooks of an event-type",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "eventTypeId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to return",
|
||||
"example": 10,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to skip",
|
||||
"example": 0,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhooksOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "EventTypeWebhooksController_deleteAllEventTypeWebhooks",
|
||||
"summary": "Delete all webhooks of an event-type",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "eventTypeId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteManyWebhooksOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/event-types/{eventTypeId}/webhooks/{webhookId}": {
|
||||
"patch": {
|
||||
"operationId": "EventTypeWebhooksController_updateEventTypeWebhook",
|
||||
"summary": "Update a webhook of an event-type",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "webhookId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateWebhookInputDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "EventTypeWebhooksController_getEventTypeWebhook",
|
||||
"summary": "Get a webhook of an event-type",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "EventTypeWebhooksController_deleteEventTypeWebhook",
|
||||
"summary": "Delete a webhook of an event-type",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
@@ -5508,57 +5841,6 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"CreateScheduleInput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "One-on-one coaching"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"example": "Europe/Rome"
|
||||
},
|
||||
"availability": {
|
||||
"example": [
|
||||
{
|
||||
"days": [
|
||||
"Monday",
|
||||
"Tuesday"
|
||||
],
|
||||
"startTime": "09:00",
|
||||
"endTime": "10:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11"
|
||||
}
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"overrides": {
|
||||
"example": [
|
||||
{
|
||||
"date": "2024-05-20",
|
||||
"startTime": "12:00",
|
||||
"endTime": "14:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"timeZone",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
"CreateScheduleOutput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -5607,52 +5889,6 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateScheduleInput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "One-on-one coaching"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"example": "Europe/Rome"
|
||||
},
|
||||
"availability": {
|
||||
"example": [
|
||||
{
|
||||
"days": [
|
||||
"Monday",
|
||||
"Tuesday"
|
||||
],
|
||||
"startTime": "09:00",
|
||||
"endTime": "10:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11"
|
||||
}
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"overrides": {
|
||||
"example": [
|
||||
{
|
||||
"date": "2024-05-20",
|
||||
"startTime": "12:00",
|
||||
"endTime": "14:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateScheduleOutput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -6747,26 +6983,6 @@
|
||||
"userId"
|
||||
]
|
||||
},
|
||||
"GetDefaultScheduleOutput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"CreateAvailabilityInput_2024_04_15": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7064,66 +7280,6 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateScheduleInput_2024_04_15": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timeZone": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"schedule": {
|
||||
"example": [
|
||||
[],
|
||||
[
|
||||
{
|
||||
"start": "2022-01-01T00:00:00.000Z",
|
||||
"end": "2022-01-02T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"items": {
|
||||
"type": "array"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"dateOverrides": {
|
||||
"example": [
|
||||
[],
|
||||
[
|
||||
{
|
||||
"start": "2022-01-01T00:00:00.000Z",
|
||||
"end": "2022-01-02T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"items": {
|
||||
"type": "array"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"timeZone",
|
||||
"name",
|
||||
"isDefault",
|
||||
"schedule"
|
||||
]
|
||||
},
|
||||
"EventTypeModel_2024_04_15": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -8425,6 +8581,285 @@
|
||||
"ReserveSlotInput": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"CreateWebhookInputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"payloadTemplate": {
|
||||
"type": "string",
|
||||
"description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
"example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}"
|
||||
},
|
||||
"triggers": {
|
||||
"type": "string",
|
||||
"example": [
|
||||
"BOOKING_CREATED",
|
||||
"BOOKING_RESCHEDULED",
|
||||
"BOOKING_CANCELLED",
|
||||
"BOOKING_CONFIRMED",
|
||||
"BOOKING_REJECTED",
|
||||
"BOOKING_COMPLETED",
|
||||
"BOOKING_NO_SHOW",
|
||||
"BOOKING_REOPENED"
|
||||
],
|
||||
"enum": [
|
||||
"BOOKING_CREATED",
|
||||
"BOOKING_PAYMENT_INITIATED",
|
||||
"BOOKING_PAID",
|
||||
"BOOKING_RESCHEDULED",
|
||||
"BOOKING_REQUESTED",
|
||||
"BOOKING_CANCELLED",
|
||||
"BOOKING_REJECTED",
|
||||
"BOOKING_NO_SHOW_UPDATED",
|
||||
"FORM_SUBMITTED",
|
||||
"MEETING_ENDED",
|
||||
"MEETING_STARTED",
|
||||
"RECORDING_READY",
|
||||
"INSTANT_MEETING",
|
||||
"RECORDING_TRANSCRIPTION_GENERATED"
|
||||
]
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subscriberUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"triggers",
|
||||
"active",
|
||||
"subscriberUrl"
|
||||
]
|
||||
},
|
||||
"UserWebhookOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"payloadTemplate": {
|
||||
"type": "string",
|
||||
"description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
"example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}"
|
||||
},
|
||||
"userId": {
|
||||
"type": "number"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"triggers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"subscriberUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"payloadTemplate",
|
||||
"userId",
|
||||
"id",
|
||||
"triggers",
|
||||
"subscriberUrl",
|
||||
"active"
|
||||
]
|
||||
},
|
||||
"UserWebhookOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/UserWebhookOutputDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateWebhookInputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"payloadTemplate": {
|
||||
"type": "string",
|
||||
"description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
"example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}"
|
||||
},
|
||||
"triggers": {
|
||||
"type": "string",
|
||||
"example": [
|
||||
"BOOKING_CREATED",
|
||||
"BOOKING_RESCHEDULED",
|
||||
"BOOKING_CANCELLED",
|
||||
"BOOKING_CONFIRMED",
|
||||
"BOOKING_REJECTED",
|
||||
"BOOKING_COMPLETED",
|
||||
"BOOKING_NO_SHOW",
|
||||
"BOOKING_REOPENED"
|
||||
],
|
||||
"enum": [
|
||||
"BOOKING_CREATED",
|
||||
"BOOKING_PAYMENT_INITIATED",
|
||||
"BOOKING_PAID",
|
||||
"BOOKING_RESCHEDULED",
|
||||
"BOOKING_REQUESTED",
|
||||
"BOOKING_CANCELLED",
|
||||
"BOOKING_REJECTED",
|
||||
"BOOKING_NO_SHOW_UPDATED",
|
||||
"FORM_SUBMITTED",
|
||||
"MEETING_ENDED",
|
||||
"MEETING_STARTED",
|
||||
"RECORDING_READY",
|
||||
"INSTANT_MEETING",
|
||||
"RECORDING_TRANSCRIPTION_GENERATED"
|
||||
]
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subscriberUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserWebhooksOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/UserWebhookOutputDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"EventTypeWebhookOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"payloadTemplate": {
|
||||
"type": "string",
|
||||
"description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
"example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}"
|
||||
},
|
||||
"eventTypeId": {
|
||||
"type": "number"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"triggers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"subscriberUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"payloadTemplate",
|
||||
"eventTypeId",
|
||||
"id",
|
||||
"triggers",
|
||||
"subscriberUrl",
|
||||
"active"
|
||||
]
|
||||
},
|
||||
"EventTypeWebhookOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhookOutputDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"EventTypeWebhooksOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhookOutputDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"DeleteManyWebhooksOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { TestingModule } from "@nestjs/testing";
|
||||
import { randomBytes, createHash } from "crypto";
|
||||
|
||||
export class ApiKeysRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TestingModule } from "@nestjs/testing";
|
||||
|
||||
export class PlatformBillingRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,16 @@ import { TestingModule } from "@nestjs/testing";
|
||||
import { Booking, User } from "@prisma/client";
|
||||
|
||||
export class BookingsRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
async getById(bookingId: Booking["id"]) {
|
||||
return this.primaReadClient.booking.findFirst({ where: { id: bookingId } });
|
||||
return this.prismaReadClient.booking.findFirst({ where: { id: bookingId } });
|
||||
}
|
||||
|
||||
async deleteById(bookingId: Booking["id"]) {
|
||||
|
||||
@@ -4,11 +4,11 @@ import { TestingModule } from "@nestjs/testing";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export class CredentialsRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ import { EventType } from "@prisma/client";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
export class EventTypesRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import { TestingModule } from "@nestjs/testing";
|
||||
import { Membership, MembershipRole, Prisma, Team, User } from "@prisma/client";
|
||||
|
||||
export class MembershipRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ export class MembershipRepositoryFixture {
|
||||
}
|
||||
|
||||
async get(membershipId: Membership["id"]) {
|
||||
return this.primaReadClient.membership.findFirst({ where: { id: membershipId } });
|
||||
return this.prismaReadClient.membership.findFirst({ where: { id: membershipId } });
|
||||
}
|
||||
|
||||
async getUserMembershipByTeamId(userId: User["id"], teamId: Team["id"]) {
|
||||
return this.primaReadClient.membership.findFirst({ where: { teamId, userId } });
|
||||
return this.prismaReadClient.membership.findFirst({ where: { teamId, userId } });
|
||||
}
|
||||
|
||||
async addUserToOrg(user: User, org: Team, role: MembershipRole, accepted: boolean) {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TestingModule } from "@nestjs/testing";
|
||||
import { Prisma, Team } from "@prisma/client";
|
||||
|
||||
export class OrganizationRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
async get(teamId: Team["id"]) {
|
||||
return this.primaReadClient.team.findFirst({ where: { id: teamId } });
|
||||
}
|
||||
|
||||
async create(data: Prisma.TeamCreateInput) {
|
||||
return await this.prismaWriteClient.$transaction(async (prisma) => {
|
||||
const team = await prisma.team.create({
|
||||
data,
|
||||
});
|
||||
|
||||
await prisma.organizationSettings.create({
|
||||
data: {
|
||||
organizationId: team.id,
|
||||
isAdminAPIEnabled: true,
|
||||
orgAutoAcceptEmail: "cal.com",
|
||||
},
|
||||
});
|
||||
return team;
|
||||
});
|
||||
}
|
||||
|
||||
async delete(teamId: Team["id"]) {
|
||||
return await this.prismaWriteClient.$transaction(async (prisma) => {
|
||||
await prisma.organizationSettings.delete({
|
||||
where: { organizationId: teamId },
|
||||
});
|
||||
return prisma.team.delete({ where: { id: teamId } });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,16 @@ import { TestingModule } from "@nestjs/testing";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export class ProfileRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
async get(profileId: number) {
|
||||
return this.primaReadClient.profile.findFirst({ where: { id: profileId } });
|
||||
return this.prismaReadClient.profile.findFirst({ where: { id: profileId } });
|
||||
}
|
||||
|
||||
async create(data: Prisma.ProfileCreateInput) {
|
||||
|
||||
@@ -6,11 +6,11 @@ import { Schedule } from "@prisma/client";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
export class SchedulesRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export class SchedulesRepositoryFixture {
|
||||
}
|
||||
|
||||
async getById(scheduleId: Schedule["id"]) {
|
||||
return this.primaReadClient.schedule.findFirst({ where: { id: scheduleId } });
|
||||
return this.prismaReadClient.schedule.findFirst({ where: { id: scheduleId } });
|
||||
}
|
||||
|
||||
async deleteById(scheduleId: Schedule["id"]) {
|
||||
|
||||
@@ -4,16 +4,16 @@ import { TestingModule } from "@nestjs/testing";
|
||||
import { Prisma, Team } from "@prisma/client";
|
||||
|
||||
export class TeamRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
async get(teamId: Team["id"]) {
|
||||
return this.primaReadClient.team.findFirst({ where: { id: teamId } });
|
||||
return this.prismaReadClient.team.findFirst({ where: { id: teamId } });
|
||||
}
|
||||
|
||||
async create(data: Prisma.TeamCreateInput) {
|
||||
@@ -25,7 +25,7 @@ export class TeamRepositoryFixture {
|
||||
}
|
||||
|
||||
async getPlatformOrgTeams(organizationId: number, oAuthClientId: string) {
|
||||
return this.primaReadClient.team.findMany({
|
||||
return this.prismaReadClient.team.findMany({
|
||||
where: {
|
||||
parentId: organizationId,
|
||||
createdByOAuthClientId: oAuthClientId,
|
||||
|
||||
@@ -5,11 +5,11 @@ import * as crypto from "crypto";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
export class TokensRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,16 @@ import { TestingModule } from "@nestjs/testing";
|
||||
import { Prisma, User } from "@prisma/client";
|
||||
|
||||
export class UserRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
async get(userId: User["id"]) {
|
||||
return this.primaReadClient.user.findFirst({ where: { id: userId } });
|
||||
return this.prismaReadClient.user.findFirst({ where: { id: userId } });
|
||||
}
|
||||
|
||||
async create(data: Prisma.UserCreateInput) {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TestingModule } from "@nestjs/testing";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export class WebhookRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
async create(data: Prisma.WebhookCreateInput) {
|
||||
return this.prismaWriteClient.webhook.create({ data });
|
||||
}
|
||||
|
||||
async delete(webhookId: string) {
|
||||
return this.prismaWriteClient.webhook.delete({ where: { id: webhookId } });
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,11 @@ export const withAppDirSsr =
|
||||
return {
|
||||
...props,
|
||||
// includes dehydratedState required for future page trpcPropvider
|
||||
...("trpcState" in props && { dehydratedState: props.trpcState }),
|
||||
...("trpcState" in props && {
|
||||
dehydratedState: props.trpcState,
|
||||
}),
|
||||
...("isInfiniteScrollEnabled" in props && {
|
||||
isInfiniteScrollEnabled: props.isInfiniteScrollEnabled,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -48,6 +48,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
||||
onSuccess: () => {
|
||||
showToast("Default app updated successfully", "success");
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
setBulkUpdateModal(true);
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(`Error: ${error.message}`, "error");
|
||||
@@ -98,7 +99,6 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
||||
updateDefaultAppMutation.mutate({
|
||||
appSlug,
|
||||
});
|
||||
setBulkUpdateModal(true);
|
||||
}
|
||||
}}>
|
||||
{t("set_as_default")}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
||||
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
|
||||
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import { ReassignDialog } from "@components/dialog/ReassignDialog";
|
||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||
|
||||
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
|
||||
@@ -155,6 +156,45 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
: []),
|
||||
];
|
||||
|
||||
const editBookingActions: ActionType[] = [
|
||||
{
|
||||
id: "reschedule",
|
||||
icon: "clock" as const,
|
||||
label: t("reschedule_booking"),
|
||||
href: `${bookerUrl}/reschedule/${booking.uid}${
|
||||
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||
}`,
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
icon: "send" as const,
|
||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||
label: t("send_reschedule_request"),
|
||||
onClick: () => {
|
||||
setIsOpenRescheduleDialog(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "change_location",
|
||||
label: t("edit_location"),
|
||||
onClick: () => {
|
||||
setIsOpenLocationDialog(true);
|
||||
},
|
||||
icon: "map-pin" as const,
|
||||
},
|
||||
];
|
||||
|
||||
if (booking.eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
|
||||
editBookingActions.push({
|
||||
id: "reassign ",
|
||||
label: t("reassign"),
|
||||
onClick: () => {
|
||||
setIsOpenReassignDialog(true);
|
||||
},
|
||||
icon: "users" as const,
|
||||
});
|
||||
}
|
||||
|
||||
let bookedActions: ActionType[] = [
|
||||
{
|
||||
id: "cancel",
|
||||
@@ -170,33 +210,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
{
|
||||
id: "edit_booking",
|
||||
label: t("edit"),
|
||||
actions: [
|
||||
{
|
||||
id: "reschedule",
|
||||
icon: "clock" as const,
|
||||
label: t("reschedule_booking"),
|
||||
href: `${bookerUrl}/reschedule/${booking.uid}${
|
||||
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||
}`,
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
icon: "send" as const,
|
||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||
label: t("send_reschedule_request"),
|
||||
onClick: () => {
|
||||
setIsOpenRescheduleDialog(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "change_location",
|
||||
label: t("edit_location"),
|
||||
onClick: () => {
|
||||
setIsOpenLocationDialog(true);
|
||||
},
|
||||
icon: "map-pin" as const,
|
||||
},
|
||||
],
|
||||
actions: editBookingActions,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -233,6 +247,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
.locale(language)
|
||||
.format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
|
||||
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
|
||||
const [isOpenReassignDialog, setIsOpenReassignDialog] = useState(false);
|
||||
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
|
||||
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
|
||||
onSuccess: () => {
|
||||
@@ -309,6 +324,12 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
setIsOpenDialog={setIsOpenRescheduleDialog}
|
||||
bookingUId={booking.uid}
|
||||
/>
|
||||
<ReassignDialog
|
||||
isOpenDialog={isOpenReassignDialog}
|
||||
setIsOpenDialog={setIsOpenReassignDialog}
|
||||
bookingId={booking.id}
|
||||
teamId={booking.eventType?.team?.id || 0}
|
||||
/>
|
||||
<EditLocationDialog
|
||||
booking={booking}
|
||||
saveLocation={saveLocation}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogFooter, showToast } from "@calcom/ui";
|
||||
|
||||
type ReassignDialog = {
|
||||
isOpenDialog: boolean;
|
||||
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
||||
teamId: number;
|
||||
bookingId: number;
|
||||
};
|
||||
|
||||
export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingId }: ReassignDialog) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const roundRobinReassignMutation = trpc.viewer.teams.roundRobinReassign.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.bookings.get.invalidate();
|
||||
setIsOpenDialog(false);
|
||||
showToast(t("booking_reassigned"), "success");
|
||||
},
|
||||
onError: async (error) => {
|
||||
if (error.message.includes(ErrorCode.NoAvailableUsersFound)) {
|
||||
showToast(t("no_available_hosts"), "error");
|
||||
} else {
|
||||
showToast(t("unexpected_error_try_again"), "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpenDialog}
|
||||
onOpenChange={(open) => {
|
||||
setIsOpenDialog(open);
|
||||
}}>
|
||||
<DialogContent title={t("reassign_round_robin_host")} description={t("reassign_to_another_rr_host")}>
|
||||
{/* TODO add team member reassignment*/}
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
data-testid="rejection-confirm"
|
||||
loading={roundRobinReassignMutation.isPending}
|
||||
onClick={() => {
|
||||
roundRobinReassignMutation.mutate({ teamId, bookingId });
|
||||
}}>
|
||||
{t("reassign")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -70,6 +70,10 @@ const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
|
||||
<p className="text-subtle ml-1 mr-5 w-32">{`{Event type title}`}</p>
|
||||
<p className="text-emphasis">{t("event_name_info")}</p>
|
||||
</div>
|
||||
<div className="mb-2.5 flex font-normal">
|
||||
<p className="text-subtle ml-1 mr-5 w-32">{`{Event duration}`}</p>
|
||||
<p className="text-emphasis">{t("event_duration_info")}</p>
|
||||
</div>
|
||||
<div className="mb-2.5 flex font-normal">
|
||||
<p className="text-subtle ml-1 mr-5 w-32">{`{Organiser}`}</p>
|
||||
<p className="text-emphasis">{t("your_full_name")}</p>
|
||||
|
||||
@@ -74,6 +74,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
eventName: formMethods.getValues("eventName"),
|
||||
host: formMethods.getValues("users")[0]?.name || "Nameless",
|
||||
bookingFields: bookingFields,
|
||||
eventDuration: formMethods.getValues("length"),
|
||||
t,
|
||||
};
|
||||
|
||||
|
||||
@@ -243,6 +243,10 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
eventLocationType.organizerInputType ||
|
||||
!validLocations?.find((location) => location.type === newLocationType);
|
||||
|
||||
const shouldUpdateLink =
|
||||
eventLocationType?.organizerInputType === "text" &&
|
||||
eventLocationType.defaultValueVariable === "link";
|
||||
|
||||
if (canAddLocation) {
|
||||
updateLocationField(index, {
|
||||
type: newLocationType,
|
||||
@@ -250,6 +254,9 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
credentialId: e.credentialId,
|
||||
teamName: e.teamName ?? undefined,
|
||||
}),
|
||||
...(shouldUpdateLink && {
|
||||
link: "",
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
updateLocationField(index, {
|
||||
|
||||
@@ -39,6 +39,7 @@ export const getEventTypesFromDB = async (id: number) => {
|
||||
bookingFields: true,
|
||||
disableGuests: true,
|
||||
timeZone: true,
|
||||
teamId: true,
|
||||
owner: {
|
||||
select: userSelect,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { ENABLE_INFINITE_EVENT_TYPES_FOR_ORG } from "@calcom/lib/constants";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
@@ -17,5 +18,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
};
|
||||
}
|
||||
|
||||
return { props: { trpcState: ssr.dehydrate() } };
|
||||
const isInfiniteScrollEnabled = session.user?.org?.slug
|
||||
? ENABLE_INFINITE_EVENT_TYPES_FOR_ORG.includes(session.user.org.slug)
|
||||
: false;
|
||||
|
||||
return { props: { trpcState: ssr.dehydrate(), isInfiniteScrollEnabled } };
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
|
||||
const isInteractionObserverSupported = typeof window !== "undefined" && "IntersectionObserver" in window;
|
||||
|
||||
export const useInViewObserver = (onInViewCallback: () => void) => {
|
||||
export const useInViewObserver = (onInViewCallback: () => void, root?: Element | Document | null) => {
|
||||
const [node, setRef] = React.useState<HTMLElement | null>(null);
|
||||
|
||||
const onInViewCallbackRef = React.useRef(onInViewCallback);
|
||||
@@ -23,7 +23,8 @@ export const useInViewObserver = (onInViewCallback: () => void) => {
|
||||
}
|
||||
},
|
||||
{
|
||||
root: document.body,
|
||||
// We want to accept null as root
|
||||
root: root !== undefined ? root : document.body,
|
||||
}
|
||||
);
|
||||
observer.observe(node);
|
||||
|
||||
@@ -33,6 +33,7 @@ function mockedSuccessComponentProps(props: Partial<React.ComponentProps<typeof
|
||||
currency: "usd",
|
||||
successRedirectUrl: null,
|
||||
customInputs: [],
|
||||
teamId: null,
|
||||
team: null,
|
||||
workflows: [],
|
||||
hosts: [],
|
||||
|
||||
@@ -233,6 +233,7 @@ export default function Success(props: PageProps) {
|
||||
host: props.profile.name || "Nameless",
|
||||
location: location,
|
||||
bookingFields: bookingInfo.responses,
|
||||
eventDuration: eventType.length,
|
||||
t,
|
||||
};
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ import {
|
||||
EmptyScreen,
|
||||
HeadSeo,
|
||||
HorizontalTabs,
|
||||
Icon,
|
||||
Label,
|
||||
showToast,
|
||||
Skeleton,
|
||||
@@ -58,25 +57,25 @@ import {
|
||||
Tooltip,
|
||||
ArrowButton,
|
||||
UserAvatarGroup,
|
||||
Icon,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import type { AppProps } from "@lib/app-providers";
|
||||
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
|
||||
|
||||
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
||||
type GetUserEventGroupsResponse = RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"];
|
||||
type GetEventTypesFromGroupsResponse = RouterOutputs["viewer"]["eventTypes"]["getEventTypesFromGroup"];
|
||||
|
||||
type InfiniteEventTypeGroup = GetUserEventGroupsResponse["eventTypeGroups"][number];
|
||||
type InfiniteEventType = GetEventTypesFromGroupsResponse["eventTypes"][number];
|
||||
|
||||
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
||||
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
||||
type GetByViewerResponse = RouterOutputs["viewer"]["eventTypes"]["getByViewer"] | undefined;
|
||||
|
||||
interface EventTypeListHeadingProps {
|
||||
profile: EventTypeGroupProfile;
|
||||
membershipCount: number;
|
||||
teamId?: number | null;
|
||||
bookerUrl: string;
|
||||
}
|
||||
|
||||
type EventTypeGroup = EventTypeGroups[number];
|
||||
type EventType = EventTypeGroup["eventTypes"][number];
|
||||
|
||||
@@ -91,6 +90,23 @@ type DeNormalizedEventTypeGroup = Omit<EventTypeGroup, "eventTypes"> & {
|
||||
eventTypes: DeNormalizedEventType[];
|
||||
};
|
||||
|
||||
const LIMIT = 10;
|
||||
|
||||
interface EventTypeListHeadingProps {
|
||||
profile: EventTypeGroupProfile;
|
||||
membershipCount: number;
|
||||
teamId?: number | null;
|
||||
bookerUrl: string;
|
||||
}
|
||||
|
||||
interface InfiniteEventTypeListProps {
|
||||
group: InfiniteEventTypeGroup;
|
||||
readOnly: boolean;
|
||||
bookerUrl: string | null;
|
||||
pages: { nextCursor: number | undefined; eventTypes: InfiniteEventType[] }[] | undefined;
|
||||
lockedByOrg?: boolean;
|
||||
}
|
||||
|
||||
interface EventTypeListProps {
|
||||
group: DeNormalizedEventTypeGroup;
|
||||
groupIndex: number;
|
||||
@@ -100,6 +116,10 @@ interface EventTypeListProps {
|
||||
lockedByOrg?: boolean;
|
||||
}
|
||||
|
||||
interface InfiniteMobileTeamsTabProps {
|
||||
activeEventTypeGroup: InfiniteEventTypeGroup;
|
||||
}
|
||||
|
||||
interface MobileTeamsTabProps {
|
||||
eventTypeGroups: DeNormalizedEventTypeGroup[];
|
||||
}
|
||||
@@ -136,13 +156,58 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const InfiniteMobileTeamsTab: FC<InfiniteMobileTeamsTabProps> = (props) => {
|
||||
const { activeEventTypeGroup } = props;
|
||||
const { t } = useLocale();
|
||||
|
||||
const query = trpc.viewer.eventTypes.getEventTypesFromGroup.useInfiniteQuery(
|
||||
{
|
||||
limit: LIMIT,
|
||||
group: { teamId: activeEventTypeGroup?.teamId, parentId: activeEventTypeGroup?.parentId },
|
||||
},
|
||||
{
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: 1 * 60 * 60 * 1000,
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
}
|
||||
);
|
||||
|
||||
const buttonInView = useInViewObserver(() => {
|
||||
if (!query.isFetching && query.hasNextPage && query.status === "success") {
|
||||
query.fetchNextPage();
|
||||
}
|
||||
}, null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!!activeEventTypeGroup && (
|
||||
<InfiniteEventTypeList
|
||||
pages={query?.data?.pages}
|
||||
group={activeEventTypeGroup}
|
||||
bookerUrl={activeEventTypeGroup.bookerUrl}
|
||||
readOnly={activeEventTypeGroup.metadata.readOnly}
|
||||
/>
|
||||
)}
|
||||
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
|
||||
<Button
|
||||
color="minimal"
|
||||
loading={query.isFetchingNextPage}
|
||||
disabled={!query.hasNextPage}
|
||||
onClick={() => query.fetchNextPage()}>
|
||||
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = ({
|
||||
type,
|
||||
group,
|
||||
readOnly,
|
||||
}: {
|
||||
type: DeNormalizedEventType;
|
||||
group: DeNormalizedEventTypeGroup;
|
||||
type: EventType | InfiniteEventType;
|
||||
group: EventTypeGroup | InfiniteEventTypeGroup;
|
||||
readOnly: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
@@ -233,11 +298,9 @@ export const EventTypeList = ({
|
||||
onError: async (err) => {
|
||||
console.error(err.message);
|
||||
await utils.viewer.eventTypes.getByViewer.cancel();
|
||||
// REVIEW: Should we invalidate the entire router or just the `getByViewer` query?
|
||||
await utils.viewer.eventTypes.invalidate();
|
||||
},
|
||||
onSettled: () => {
|
||||
// REVIEW: Should we invalidate the entire router or just the `getByViewer` query?
|
||||
utils.viewer.eventTypes.invalidate();
|
||||
},
|
||||
});
|
||||
@@ -323,6 +386,8 @@ export const EventTypeList = ({
|
||||
setParamsIfDefined("id", eventType.id);
|
||||
setParamsIfDefined("length", eventType.length);
|
||||
setParamsIfDefined("pageSlug", group.profile.slug);
|
||||
setParamsIfDefined("teamId", eventType.teamId);
|
||||
setParamsIfDefined("parentId", eventType.parentId);
|
||||
router.push(`${pathname}?${newSearchParams.toString()}`);
|
||||
};
|
||||
|
||||
@@ -714,6 +779,546 @@ export const EventTypeList = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const InfiniteEventTypeList = ({
|
||||
group,
|
||||
readOnly,
|
||||
pages,
|
||||
bookerUrl,
|
||||
lockedByOrg,
|
||||
}: InfiniteEventTypeListProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
|
||||
const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = useState<SchedulingType | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
const mutation = trpc.viewer.eventTypeOrder.useMutation({
|
||||
onError: async (err) => {
|
||||
console.error(err.message);
|
||||
// REVIEW: Should we invalidate the entire router or just the `getByViewer` query?
|
||||
await utils.viewer.eventTypes.getEventTypesFromGroup.cancel();
|
||||
},
|
||||
});
|
||||
|
||||
const setHiddenMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||
onMutate: async (data) => {
|
||||
await utils.viewer.eventTypes.getEventTypesFromGroup.cancel();
|
||||
const previousValue = utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({
|
||||
limit: LIMIT,
|
||||
group: { teamId: group?.teamId, parentId: group?.parentId },
|
||||
});
|
||||
|
||||
if (previousValue) {
|
||||
pages?.forEach((page) => {
|
||||
page?.eventTypes?.forEach((eventType) => {
|
||||
if (eventType.id === data.id) {
|
||||
eventType.hidden = !eventType.hidden;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return { previousValue };
|
||||
},
|
||||
onError: async (err, _, context) => {
|
||||
if (context?.previousValue) {
|
||||
utils.viewer.eventTypes.getEventTypesFromGroup.setInfiniteData(
|
||||
{ limit: LIMIT, group: { teamId: group?.teamId, parentId: group?.parentId } },
|
||||
context.previousValue
|
||||
);
|
||||
}
|
||||
console.error(err.message);
|
||||
},
|
||||
});
|
||||
|
||||
async function moveEventType(index: number, increment: 1 | -1) {
|
||||
if (!pages) return;
|
||||
const newOrder = pages;
|
||||
const pageNo = Math.floor(index / LIMIT);
|
||||
|
||||
const currentPositionEventType = newOrder[pageNo].eventTypes[index % LIMIT];
|
||||
|
||||
const newPageNo =
|
||||
increment === -1
|
||||
? pageNo > 0 && index % LIMIT === 0
|
||||
? pageNo - 1
|
||||
: pageNo
|
||||
: index % LIMIT === LIMIT - 1
|
||||
? pageNo + 1
|
||||
: pageNo;
|
||||
|
||||
const newIdx = (index + increment) % LIMIT;
|
||||
const newPositionEventType = newOrder[newPageNo].eventTypes[newIdx];
|
||||
|
||||
newOrder[pageNo].eventTypes[index % LIMIT] = newPositionEventType;
|
||||
newOrder[newPageNo].eventTypes[newIdx] = currentPositionEventType;
|
||||
|
||||
await utils.viewer.eventTypes.getEventTypesFromGroup.cancel();
|
||||
const previousValue = utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({
|
||||
limit: LIMIT,
|
||||
group: { teamId: group?.teamId, parentId: group?.parentId },
|
||||
});
|
||||
|
||||
if (previousValue) {
|
||||
utils.viewer.eventTypes.getEventTypesFromGroup.setInfiniteData(
|
||||
{ limit: LIMIT, group: { teamId: group?.teamId, parentId: group?.parentId } },
|
||||
(data) => {
|
||||
return {
|
||||
pageParams: data?.pageParams ?? [],
|
||||
pages: newOrder,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
mutation.mutate({
|
||||
ids: newOrder.flatMap((page) => page.eventTypes.map((type) => type.id)),
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteEventTypeHandler(id: number) {
|
||||
const payload = { id };
|
||||
deleteMutation.mutate(payload);
|
||||
}
|
||||
|
||||
// inject selection data into url for correct router history
|
||||
const openDuplicateModal = (eventType: InfiniteEventType, group: InfiniteEventTypeGroup) => {
|
||||
const newSearchParams = new URLSearchParams(searchParams ?? undefined);
|
||||
function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) {
|
||||
if (value) newSearchParams.set(key, value.toString());
|
||||
if (value === null) newSearchParams.delete(key);
|
||||
}
|
||||
setParamsIfDefined("dialog", "duplicate");
|
||||
setParamsIfDefined("title", eventType.title);
|
||||
setParamsIfDefined("description", eventType.description);
|
||||
setParamsIfDefined("slug", eventType.slug);
|
||||
setParamsIfDefined("id", eventType.id);
|
||||
setParamsIfDefined("length", eventType.length);
|
||||
setParamsIfDefined("pageSlug", group.profile.slug);
|
||||
router.push(`${pathname}?${newSearchParams.toString()}`);
|
||||
};
|
||||
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
setDeleteDialogOpen(false);
|
||||
},
|
||||
onMutate: async ({ id }) => {
|
||||
await utils.viewer.eventTypes.getEventTypesFromGroup.cancel();
|
||||
const previousValue = utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({
|
||||
limit: LIMIT,
|
||||
group: { teamId: group?.teamId, parentId: group?.parentId },
|
||||
});
|
||||
|
||||
if (previousValue) {
|
||||
await utils.viewer.eventTypes.getEventTypesFromGroup.setInfiniteData(
|
||||
{ limit: LIMIT, group: { teamId: group?.teamId, parentId: group?.parentId } },
|
||||
(data) => {
|
||||
if (!data) {
|
||||
return {
|
||||
pages: [],
|
||||
pageParams: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...data,
|
||||
pages: data.pages.map((page) => ({
|
||||
...page,
|
||||
eventTypes: page.eventTypes.filter((type) => type.id !== id),
|
||||
})),
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return { previousValue };
|
||||
},
|
||||
onError: (err, _, context) => {
|
||||
if (context?.previousValue) {
|
||||
utils.viewer.eventTypes.getEventTypesFromGroup.setInfiniteData(
|
||||
{ limit: LIMIT, group: { teamId: group?.teamId, parentId: group?.parentId } },
|
||||
context.previousValue
|
||||
);
|
||||
}
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
setDeleteDialogOpen(false);
|
||||
} else if (err instanceof TRPCClientError) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [isNativeShare, setNativeShare] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!navigator.share) {
|
||||
setNativeShare(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!pages?.[0]?.eventTypes?.length) {
|
||||
return group.teamId ? (
|
||||
<EmptyEventTypeList group={group} />
|
||||
) : !group.profile.eventTypesLockedByOrg ? (
|
||||
<CreateFirstEventTypeView slug={group.profile.slug ?? ""} />
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
const firstItem = pages?.[0]?.eventTypes[0];
|
||||
const lastItem = pages?.[pages.length - 1]?.eventTypes[pages?.[pages.length - 1].eventTypes.length - 1];
|
||||
const isManagedEventPrefix = () => {
|
||||
return deleteDialogTypeSchedulingType === SchedulingType.MANAGED ? "_managed" : "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-default border-subtle flex flex-col overflow-hidden rounded-md border">
|
||||
<ul ref={parent} className="divide-subtle !static w-full divide-y" data-testid="event-types">
|
||||
{pages.map((page, pageIdx) => {
|
||||
return page?.eventTypes?.map((type, index) => {
|
||||
const embedLink = `${group.profile.slug}/${type.slug}`;
|
||||
const calLink = `${bookerUrl}/${embedLink}`;
|
||||
const isPrivateURLEnabled = type.hashedLink?.link;
|
||||
const placeholderHashedLink = `${WEBSITE_URL}/d/${type.hashedLink?.link}/${type.slug}`;
|
||||
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
|
||||
const isChildrenManagedEventType =
|
||||
type.metadata?.managedEventConfig !== undefined &&
|
||||
type.schedulingType !== SchedulingType.MANAGED;
|
||||
return (
|
||||
<li key={type.id}>
|
||||
<div className="hover:bg-muted flex w-full items-center justify-between transition">
|
||||
<div className="group flex w-full max-w-full items-center justify-between overflow-hidden px-4 py-4 sm:px-6">
|
||||
{!(firstItem && firstItem.id === type.id) && (
|
||||
<ArrowButton
|
||||
onClick={() => moveEventType(LIMIT * pageIdx + index, -1)}
|
||||
arrowDirection="up"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!(lastItem && lastItem.id === type.id) && (
|
||||
<ArrowButton
|
||||
onClick={() => moveEventType(LIMIT * pageIdx + index, 1)}
|
||||
arrowDirection="down"
|
||||
/>
|
||||
)}
|
||||
<MemoizedItem type={type} group={group} readOnly={readOnly} />
|
||||
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
{!!type.teamId && !isManagedEventType && (
|
||||
<UserAvatarGroup
|
||||
className="relative right-3"
|
||||
size="sm"
|
||||
truncateAfter={4}
|
||||
hideTruncatedAvatarsCount={true}
|
||||
users={type?.users ?? []}
|
||||
/>
|
||||
)}
|
||||
{isManagedEventType && type?.children && type.children?.length > 0 && (
|
||||
<UserAvatarGroup
|
||||
className="relative right-3"
|
||||
size="sm"
|
||||
truncateAfter={4}
|
||||
hideTruncatedAvatarsCount={true}
|
||||
users={type?.children.flatMap((ch) => ch.users) ?? []}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
|
||||
{!isManagedEventType && (
|
||||
<>
|
||||
{type.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
|
||||
<Tooltip
|
||||
content={
|
||||
type.hidden ? t("show_eventtype_on_profile") : t("hide_from_profile")
|
||||
}>
|
||||
<div className="self-center rounded-md p-2">
|
||||
<Switch
|
||||
name="Hidden"
|
||||
disabled={lockedByOrg}
|
||||
checked={!type.hidden}
|
||||
onCheckedChange={() => {
|
||||
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ButtonGroup combined>
|
||||
{!isManagedEventType && (
|
||||
<>
|
||||
<Tooltip content={t("preview")}>
|
||||
<Button
|
||||
data-testid="preview-link-button"
|
||||
color="secondary"
|
||||
target="_blank"
|
||||
variant="icon"
|
||||
href={calLink}
|
||||
StartIcon="external-link"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={t("copy_link")}>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon="link"
|
||||
onClick={() => {
|
||||
showToast(t("link_copied"), "success");
|
||||
navigator.clipboard.writeText(calLink);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{isPrivateURLEnabled && (
|
||||
<Tooltip content={t("copy_private_link_to_event")}>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon="venetian-mask"
|
||||
onClick={() => {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild data-testid={`event-type-options-${type.id}`}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="icon"
|
||||
color="secondary"
|
||||
StartIcon="ellipsis"
|
||||
className="ltr:radix-state-open:rounded-r-md rtl:radix-state-open:rounded-l-md"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{!readOnly && (
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
data-testid={`event-type-edit-${type.id}`}
|
||||
StartIcon="pencil"
|
||||
onClick={() => router.push(`/event-types/${type.id}`)}>
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
||||
{!readOnly && !isManagedEventType && !isChildrenManagedEventType && (
|
||||
<>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
type="button"
|
||||
data-testid={`event-type-duplicate-${type.id}`}
|
||||
StartIcon="copy"
|
||||
onClick={() => openDuplicateModal(type, group)}>
|
||||
{t("duplicate")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{!isManagedEventType && (
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<EventTypeEmbedButton
|
||||
namespace={type.slug}
|
||||
as={DropdownItem}
|
||||
type="button"
|
||||
StartIcon="code"
|
||||
className="w-full rounded-none"
|
||||
embedUrl={encodeURIComponent(embedLink)}
|
||||
eventId={type.id}>
|
||||
{t("embed")}
|
||||
</EventTypeEmbedButton>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
||||
{!readOnly && !isChildrenManagedEventType && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
setDeleteDialogTypeId(type.id);
|
||||
setDeleteDialogSchedulingType(type.schedulingType);
|
||||
}}
|
||||
StartIcon="trash"
|
||||
className="w-full rounded-none">
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-9 mx-5 flex sm:hidden">
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild data-testid={`event-type-options-${type.id}`}>
|
||||
<Button type="button" variant="icon" color="secondary" StartIcon="ellipsis" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>
|
||||
{!isManagedEventType && (
|
||||
<>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
href={calLink}
|
||||
target="_blank"
|
||||
StartIcon="external-link"
|
||||
className="w-full rounded-none">
|
||||
{t("preview")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
data-testid={`event-type-duplicate-${type.id}`}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(calLink);
|
||||
showToast(t("link_copied"), "success");
|
||||
}}
|
||||
StartIcon="clipboard"
|
||||
className="w-full rounded-none text-left">
|
||||
{t("copy_link")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{isNativeShare ? (
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
data-testid={`event-type-duplicate-${type.id}`}
|
||||
onClick={() => {
|
||||
navigator
|
||||
.share({
|
||||
title: t("share"),
|
||||
text: t("share_event", { appName: APP_NAME }),
|
||||
url: calLink,
|
||||
})
|
||||
.then(() => showToast(t("link_shared"), "success"))
|
||||
.catch(() => showToast(t("failed"), "error"));
|
||||
}}
|
||||
StartIcon="upload"
|
||||
className="w-full rounded-none">
|
||||
{t("share")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{!readOnly && (
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
onClick={() => router.push(`/event-types/${type.id}`)}
|
||||
StartIcon="pencil"
|
||||
className="w-full rounded-none">
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{!readOnly && !isManagedEventType && !isChildrenManagedEventType && (
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
onClick={() => openDuplicateModal(type, group)}
|
||||
StartIcon="copy"
|
||||
data-testid={`event-type-duplicate-${type.id}`}>
|
||||
{t("duplicate")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
||||
{!readOnly && !isChildrenManagedEventType && (
|
||||
<>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
setDeleteDialogTypeId(type.id);
|
||||
setDeleteDialogSchedulingType(type.schedulingType);
|
||||
}}
|
||||
StartIcon="trash"
|
||||
className="w-full rounded-none">
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
{!isManagedEventType && (
|
||||
<div className="hover:bg-subtle flex h-9 cursor-pointer flex-row items-center justify-between px-4 py-2">
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
className="mt-2 inline cursor-pointer self-center pr-2 ">
|
||||
{type.hidden ? t("show_eventtype_on_profile") : t("hide_from_profile")}
|
||||
</Skeleton>
|
||||
<Switch
|
||||
id="hiddenSwitch"
|
||||
name="Hidden"
|
||||
checked={!type.hidden}
|
||||
onCheckedChange={() => {
|
||||
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<ConfirmationDialogContent
|
||||
variety="danger"
|
||||
title={t(`delete${isManagedEventPrefix()}_event_type`)}
|
||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||
loadingText={t(`confirm_delete_event_type`)}
|
||||
isPending={deleteMutation.isPending}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
deleteEventTypeHandler(deleteDialogTypeId);
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey={`delete${isManagedEventPrefix()}_event_type_description`}
|
||||
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||
<ul>
|
||||
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||
<li>
|
||||
Anyone who they've shared their link with will no longer be able to book using it.
|
||||
</li>
|
||||
</ul>
|
||||
</Trans>
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EventTypeListHeading = ({
|
||||
profile,
|
||||
membershipCount,
|
||||
@@ -795,6 +1400,7 @@ const CreateFirstEventTypeView = ({ slug }: { slug: string }) => {
|
||||
const CTA = ({
|
||||
profileOptions,
|
||||
isOrganization,
|
||||
isInfiniteScrollEnabled,
|
||||
}: {
|
||||
profileOptions: {
|
||||
teamId: number | null | undefined;
|
||||
@@ -804,6 +1410,7 @@ const CTA = ({
|
||||
slug: string | null;
|
||||
}[];
|
||||
isOrganization: boolean;
|
||||
isInfiniteScrollEnabled: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -815,7 +1422,10 @@ const CTA = ({
|
||||
subtitle={t("create_event_on").toUpperCase()}
|
||||
options={profileOptions}
|
||||
createDialog={() => (
|
||||
<CreateEventTypeDialog profileOptions={profileOptions} isOrganization={isOrganization} />
|
||||
<CreateEventTypeDialog
|
||||
isInfiniteScrollEnabled={isInfiniteScrollEnabled}
|
||||
profileOptions={profileOptions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -829,7 +1439,7 @@ const Actions = (props: { showDivider: boolean }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyEventTypeList = ({ group }: { group: DeNormalizedEventTypeGroup }) => {
|
||||
const EmptyEventTypeList = ({ group }: { group: EventTypeGroup | InfiniteEventTypeGroup }) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
@@ -941,10 +1551,56 @@ const Main = ({
|
||||
);
|
||||
};
|
||||
|
||||
const EventTypesPage: React.FC & {
|
||||
const InfiniteScrollMain = ({
|
||||
status,
|
||||
errorMessage,
|
||||
eventTypeGroups,
|
||||
profiles,
|
||||
}: {
|
||||
status: string;
|
||||
errorMessage?: string;
|
||||
eventTypeGroups: GetUserEventGroupsResponse["eventTypeGroups"] | undefined;
|
||||
profiles: GetUserEventGroupsResponse["profiles"] | undefined;
|
||||
}) => {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { data } = useTypedQuery(querySchema);
|
||||
|
||||
if (status === "error") {
|
||||
return <Alert severity="error" title="Something went wrong" message={errorMessage} />;
|
||||
}
|
||||
|
||||
if (!eventTypeGroups || !profiles || status === "pending") {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
const tabs = eventTypeGroups.map((item) => ({
|
||||
name: item.profile.name ?? "",
|
||||
href: item.teamId ? `/event-types?teamId=${item.teamId}` : "/event-types?noTeam",
|
||||
avatar: item.profile.image,
|
||||
}));
|
||||
|
||||
const activeEventTypeGroup =
|
||||
eventTypeGroups.filter((item) => item.teamId === data.teamId) ?? eventTypeGroups[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
{eventTypeGroups.length >= 1 && (
|
||||
<>
|
||||
<HorizontalTabs tabs={tabs} />
|
||||
<InfiniteMobileTeamsTab activeEventTypeGroup={activeEventTypeGroup[0]} />
|
||||
</>
|
||||
)}
|
||||
{eventTypeGroups.length === 0 && <CreateFirstEventTypeView slug={profiles[0].slug ?? ""} />}
|
||||
<EventTypeEmbedDialog />
|
||||
{searchParams?.get("dialog") === "duplicate" && <DuplicateDialog isInfiniteScrollEnabled />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EventTypesPage: React.FC<{ isInfiniteScrollEnabled?: boolean }> & {
|
||||
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||
getLayout?: AppProps["Component"]["getLayout"];
|
||||
} = () => {
|
||||
} = ({ isInfiniteScrollEnabled = false }) => {
|
||||
const { t } = useLocale();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { open } = useIntercom();
|
||||
@@ -956,11 +1612,23 @@ const EventTypesPage: React.FC & {
|
||||
const filters = getTeamsFiltersFromQuery(routerQuery);
|
||||
const router = useRouter();
|
||||
|
||||
// TODO: Maybe useSuspenseQuery to focus on success case only? Remember that it would crash the page when there is an error in query. Also, it won't support skeleton
|
||||
const { data, status, error } = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters }, {
|
||||
refetchOnWindowFocus: false,
|
||||
gcTime: 1 * 60 * 60 * 1000,
|
||||
staleTime: 1 * 60 * 60 * 1000,
|
||||
enabled: !isInfiniteScrollEnabled,
|
||||
});
|
||||
|
||||
// TODO: Maybe useSuspenseQuery to focus on success case only? Remember that it would crash the page when there is an error in query. Also, it won't support skeleton
|
||||
const {
|
||||
data: getUserEventGroupsData,
|
||||
status: getUserEventGroupsStatus,
|
||||
error: getUserEventGroupsStatusError,
|
||||
} = trpc.viewer.eventTypes.getUserEventGroups.useQuery(filters && { filters }, {
|
||||
refetchOnWindowFocus: false,
|
||||
gcTime: 1 * 60 * 60 * 1000,
|
||||
staleTime: 1 * 60 * 60 * 1000,
|
||||
enabled: isInfiniteScrollEnabled,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -983,20 +1651,32 @@ const EventTypesPage: React.FC & {
|
||||
);
|
||||
}, [orgBranding, user]);
|
||||
|
||||
const profileOptions = data
|
||||
? data?.profiles
|
||||
.filter((profile) => !profile.readOnly)
|
||||
.filter((profile) => !profile.eventTypesLockedByOrg)
|
||||
.map((profile) => {
|
||||
return {
|
||||
teamId: profile.teamId,
|
||||
label: profile.name || profile.slug,
|
||||
image: profile.image,
|
||||
membershipRole: profile.membershipRole,
|
||||
slug: profile.slug,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
const profileOptions =
|
||||
(data?.profiles
|
||||
? data?.profiles
|
||||
.filter((profile) => !profile.readOnly)
|
||||
.filter((profile) => !profile.eventTypesLockedByOrg)
|
||||
.map((profile) => {
|
||||
return {
|
||||
teamId: profile.teamId,
|
||||
label: profile.name || profile.slug,
|
||||
image: profile.image,
|
||||
membershipRole: profile.membershipRole,
|
||||
slug: profile.slug,
|
||||
};
|
||||
})
|
||||
: getUserEventGroupsData?.profiles
|
||||
?.filter((profile) => !profile.readOnly)
|
||||
?.filter((profile) => !profile.eventTypesLockedByOrg)
|
||||
?.map((profile) => {
|
||||
return {
|
||||
teamId: profile.teamId,
|
||||
label: profile.name || profile.slug,
|
||||
image: profile.image,
|
||||
membershipRole: profile.membershipRole,
|
||||
slug: profile.slug,
|
||||
};
|
||||
})) ?? [];
|
||||
|
||||
return (
|
||||
<Shell
|
||||
@@ -1007,13 +1687,30 @@ const EventTypesPage: React.FC & {
|
||||
heading={t("event_types_page_title")}
|
||||
hideHeadingOnMobile
|
||||
subtitle={t("event_types_page_subtitle")}
|
||||
beforeCTAactions={<Actions showDivider={profileOptions.length > 0} />}
|
||||
CTA={<CTA profileOptions={profileOptions} isOrganization={!!user?.organizationId} />}>
|
||||
beforeCTAactions={
|
||||
isInfiniteScrollEnabled ? undefined : <Actions showDivider={profileOptions.length > 0} />
|
||||
}
|
||||
CTA={
|
||||
<CTA
|
||||
isInfiniteScrollEnabled={isInfiniteScrollEnabled}
|
||||
profileOptions={profileOptions}
|
||||
isOrganization={!!user?.organizationId}
|
||||
/>
|
||||
}>
|
||||
<HeadSeo
|
||||
title="Event Types"
|
||||
description="Create events to share for people to book on your calendar."
|
||||
/>
|
||||
<Main data={data} status={status} errorMessage={error?.message} filters={filters} />
|
||||
{isInfiniteScrollEnabled ? (
|
||||
<InfiniteScrollMain
|
||||
profiles={getUserEventGroupsData?.profiles}
|
||||
eventTypeGroups={getUserEventGroupsData?.eventTypeGroups}
|
||||
status={getUserEventGroupsStatus}
|
||||
errorMessage={getUserEventGroupsStatusError?.message}
|
||||
/>
|
||||
) : (
|
||||
<Main data={data} status={status} errorMessage={error?.message} filters={filters} />
|
||||
)}
|
||||
</Shell>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -172,7 +172,7 @@ const matcherConfigUserTypeEmbedRoute = {
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
output: process.env.BUILD_STANDALONE === "true" ? "standalone" : undefined,
|
||||
experimental: {
|
||||
// externalize server-side node_modules with size > 1mb, to improve dev mode performance/RAM usage
|
||||
serverComponentsExternalPackages: ["next-i18next"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "4.3.4",
|
||||
"version": "4.3.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -144,7 +144,7 @@
|
||||
"@calcom/config": "*",
|
||||
"@calcom/types": "*",
|
||||
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
|
||||
"@playwright/test": "^1.31.2",
|
||||
"@playwright/test": "^1.45.3",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@types/accept-language-parser": "1.5.2",
|
||||
"@types/async": "^3.2.15",
|
||||
|
||||
+1
-25
@@ -9,11 +9,10 @@ import {
|
||||
getOrgDomainConfigFromHostname,
|
||||
subdomainSuffix,
|
||||
} from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DOCS_URL, IS_CALCOM, JOIN_DISCORD, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { DOCS_URL, IS_CALCOM, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { Discord } from "@calcom/ui/components/icon/Discord";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
@@ -233,29 +232,6 @@ export default function Custom404() {
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li className="px-4 py-2">
|
||||
<a
|
||||
href={JOIN_DISCORD}
|
||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
|
||||
<Discord className="text-default h-6 w-6" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-emphasis text-base font-medium">
|
||||
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
Discord
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-subtle text-base">{t("join_our_community")}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<Icon name="chevron-right" className="text-muted h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-8">
|
||||
<Link href={WEBSITE_URL} className="hover:text-subtle text-emphasis text-base font-medium">
|
||||
|
||||
@@ -433,7 +433,7 @@ export default function Signup({
|
||||
<div className="mt-6">
|
||||
<div className="relative flex items-center">
|
||||
<div className="border-subtle flex-grow border-t" />
|
||||
<span className="text-subtle leadning-none mx-2 flex-shrink text-sm font-normal ">
|
||||
<span className="text-subtle mx-2 flex-shrink text-sm font-normal leading-none">
|
||||
{t("or_continue_with")}
|
||||
</span>
|
||||
<div className="border-subtle flex-grow border-t" />
|
||||
|
||||
@@ -6,7 +6,7 @@ import { randomString } from "@calcom/lib/random";
|
||||
import { SchedulingType } from "@calcom/prisma/client";
|
||||
import type { Schedule, TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { test, todo } from "./lib/fixtures";
|
||||
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
|
||||
import {
|
||||
bookFirstEvent,
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
testEmail,
|
||||
testName,
|
||||
todo,
|
||||
} from "./lib/testUtils";
|
||||
|
||||
const freeUserObj = { name: `Free-user-${randomString(3)}` };
|
||||
|
||||
@@ -5,7 +5,7 @@ import { test } from "./lib/fixtures";
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Users can impersonate", async () => {
|
||||
test.afterAll(async ({ users }) => {
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
test("App Admin can impersonate users with impersonation enabled", async ({ page, users }) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user