Compare commits
119
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e6ad8be9a | ||
|
|
1aca18324d | ||
|
|
a8a3e48465 | ||
|
|
2e96600529 | ||
|
|
b9074aeb81 | ||
|
|
13add03479 | ||
|
|
bb2dcc51c6 | ||
|
|
8f82e78597 | ||
|
|
34d2a13080 | ||
|
|
6066ed206b | ||
|
|
d30d2792cd | ||
|
|
ba50268634 | ||
|
|
4ca78f46df | ||
|
|
3a7ca0246c | ||
|
|
2cc8b1c2af | ||
|
|
a2d0cbfe5b | ||
|
|
d65c0753ef | ||
|
|
76bd90b228 | ||
|
|
d908403a99 | ||
|
|
0fe775962f | ||
|
|
412a6992c7 | ||
|
|
cfd1992733 | ||
|
|
e86d6169ba | ||
|
|
a379bd2b52 | ||
|
|
1137047606 | ||
|
|
ad4cf0cc3a | ||
|
|
e7d2e0dc2f | ||
|
|
aa51218428 | ||
|
|
38bc5fb71e | ||
|
|
411516ddb9 | ||
|
|
c6c17ac388 | ||
|
|
6838133865 | ||
|
|
4b2bb2271a | ||
|
|
20b7ac8c0d | ||
|
|
ca7c432728 | ||
|
|
3b966191e3 | ||
|
|
a7889984a0 | ||
|
|
3c72d7aaa0 | ||
|
|
ece0030978 | ||
|
|
a26a769119 | ||
|
|
3313de1eec | ||
|
|
7a6e16631b | ||
|
|
ce4d71ba2e | ||
|
|
300cd31efb | ||
|
|
f5bb734441 | ||
|
|
e33560ef82 | ||
|
|
84f34b2244 | ||
|
|
086bae03d3 | ||
|
|
4a0a429774 | ||
|
|
c4142ab4e7 | ||
|
|
95f8046db9 | ||
|
|
d8a87e9f04 | ||
|
|
86dcd6f0ff | ||
|
|
411a14dd3d | ||
|
|
03bc8db64a | ||
|
|
fe7fd8bc79 | ||
|
|
92eb41c117 | ||
|
|
a1fca56aac | ||
|
|
f3630399ee | ||
|
|
5375c656af | ||
|
|
1792d49634 | ||
|
|
de98578fdd | ||
|
|
c0a59a2837 | ||
|
|
29201006d5 | ||
|
|
7a1c780088 | ||
|
|
550f34c128 | ||
|
|
d6b17b76db | ||
|
|
c42e803cfa | ||
|
|
0233c3a11a | ||
|
|
71a2b6956f | ||
|
|
8282557ebe | ||
|
|
083a162d0e | ||
|
|
60487ea02e | ||
|
|
60a8841439 | ||
|
|
184d1eb751 | ||
|
|
9fea30a44d | ||
|
|
04efcbcdb6 | ||
|
|
2b29b5607d | ||
|
|
20f19c2509 | ||
|
|
5d0e66763e | ||
|
|
bcfbc24e93 | ||
|
|
37d805b007 | ||
|
|
6a246e6c57 | ||
|
|
3a0229f60d | ||
|
|
0bf95ec568 | ||
|
|
6f05142447 | ||
|
|
e02108d535 | ||
|
|
b7e668217d | ||
|
|
472ffef5cf | ||
|
|
5acdb208f5 | ||
|
|
7bf5d9cf64 | ||
|
|
34a80180ef | ||
|
|
3bbba9c6cb | ||
|
|
b052c2e029 | ||
|
|
4eb884e2a6 | ||
|
|
3f8ce8ff50 | ||
|
|
67a2371c05 | ||
|
|
80250d32bd | ||
|
|
0fadbe1ce9 | ||
|
|
9c3ee262b4 | ||
|
|
b62eba8128 | ||
|
|
ef114d7727 | ||
|
|
be7d127c9e | ||
|
|
1209150785 | ||
|
|
31940b919e | ||
|
|
c3c990db43 | ||
|
|
84a742336c | ||
|
|
ada8ffd3b9 | ||
|
|
2277d90326 | ||
|
|
cbc27a78ee | ||
|
|
a2189643bd | ||
|
|
a6101b3da9 | ||
|
|
216fb2141b | ||
|
|
afb54ca048 | ||
|
|
f4b23b3a03 | ||
|
|
d6cd593b95 | ||
|
|
2d9c1b3fd9 | ||
|
|
f2f10a06d9 | ||
|
|
e1def884f3 |
@@ -318,3 +318,12 @@ dates.map((date) => dayjs.utc(date).add(1, "day").format());
|
||||
// ✅ Best - Use native Date when possible
|
||||
dates.map((date) => new Date(date.valueOf() + 24 * 60 * 60 * 1000));
|
||||
```
|
||||
|
||||
## Avoid using Dayjs if you don’t need to be strictly tz aware.
|
||||
|
||||
When doing logic like Dayjs.startOf(".."), you can instead use date-fns' `startOfMonth(dateObj)` / `endOfDay(dateObj)`;
|
||||
When doing logic that depends on Browser locale, use i18n.language (prefer to deconstruct) like: `const { i18n: { language } } = useLocale();`, in combination with built-in Intl.
|
||||
|
||||
Note that with Date, you’re dealing with System time, so it’s not suited to everywhere (such as in the Booker, where instead we’ll likely migrate to Temporal) - but in most cases the above are suitable.
|
||||
|
||||
The main reason for doing so is that Dayjs uses a useful, but highly risky plugin system, which has led us to create `@calcom/dayjs` - this is heavy however, because it pre-loads ALL plugins, including locale handling. It’s a non-ideal solution to a problem that unfortunately exists due to Dayjs.
|
||||
|
||||
+8
-8
@@ -103,19 +103,14 @@ NEXT_PUBLIC_INTERCOM_APP_ID=
|
||||
# Secret to enable Intercom Identity Verification
|
||||
INTERCOM_SECRET=
|
||||
|
||||
# Intercom api token for free users
|
||||
INTERCOM_API_TOKEN=
|
||||
|
||||
# Posthog Config
|
||||
NEXT_PUBLIC_POSTHOG_KEY=
|
||||
|
||||
NEXT_PUBLIC_POSTHOG_HOST=
|
||||
|
||||
# plain.com config
|
||||
|
||||
PLAIN_API_KEY=
|
||||
PLAIN_API_URL=https://api.plain.com/v1
|
||||
PLAIN_HMAC_SECRET_KEY=
|
||||
NEXT_PUBLIC_PLAIN_CHAT_ID=
|
||||
PLAIN_CHAT_HMAC_SECRET_KEY=
|
||||
NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS=
|
||||
|
||||
|
||||
# Dub Config
|
||||
@@ -386,6 +381,11 @@ UNKEY_ROOT_KEY=
|
||||
# Used for Cal.ai Voice AI Agents
|
||||
# https://retellai.com
|
||||
RETELL_AI_KEY=
|
||||
# Local testing overrides for Retell AI
|
||||
RETELL_AI_TEST_MODE=false
|
||||
# JSON mapping of local to production event type IDs (e.g. {"50": 747280} without any string)
|
||||
RETELL_AI_TEST_EVENT_TYPE_MAP=
|
||||
RETELL_AI_TEST_CAL_API_KEY=
|
||||
|
||||
# Used for buying phone number for cal ai voice agent
|
||||
STRIPE_PHONE_NUMBER_MONTHLY_PRICE_ID=
|
||||
|
||||
@@ -19,5 +19,17 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"warn",
|
||||
{
|
||||
paths: ["@calcom/trpc"],
|
||||
patterns: ["@calcom/trpc/*"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
with:
|
||||
filters: |
|
||||
has-files-requiring-all-checks:
|
||||
- "!(**.md|.github/CODEOWNERS|docs/**|help/**|apps/web/public/static/locales/**/common.json)"
|
||||
- "!(**.md|**.mdx|.github/CODEOWNERS|docs/**|help/**|apps/web/public/static/locales/**/common.json|i18n.lock)"
|
||||
- name: Get Latest Commit SHA
|
||||
id: get_sha
|
||||
run: |
|
||||
|
||||
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
Vendored
Vendored
@@ -43,7 +43,7 @@ export const bookingCancelSchema = z.object({
|
||||
id: z.number(),
|
||||
allRemainingBookings: z.boolean().optional(),
|
||||
cancelSubsequentBookings: z.boolean().optional(),
|
||||
cancellationReason: z.string().optional().default("Not Provided"),
|
||||
cancellationReason: z.string().min(1).optional(),
|
||||
seatReferenceUid: z.string().optional(),
|
||||
cancelledBy: z.string().email({ message: "Invalid email" }).optional(),
|
||||
internalNote: z
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { NextApiRequest } from "next";
|
||||
import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
import { schemaCredentialPatchParams, schemaCredentialPatchBody } from "~/lib/validations/credential-sync";
|
||||
|
||||
@@ -71,7 +72,7 @@ async function handler(req: NextApiRequest) {
|
||||
userId,
|
||||
},
|
||||
data: {
|
||||
key,
|
||||
key: key as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -6,7 +6,8 @@ import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
import { schemaCredentialPostBody, schemaCredentialPostParams } from "~/lib/validations/credential-sync";
|
||||
@@ -92,7 +93,7 @@ async function handler(req: NextApiRequest) {
|
||||
data: {
|
||||
userId,
|
||||
appId: appSlug,
|
||||
key,
|
||||
key: key as unknown as Prisma.InputJsonValue,
|
||||
type: appMetadata.type,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { NextApiRequest } from "next";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("POST /api/attendees", () => {
|
||||
|
||||
const userBooking = { id: 1 };
|
||||
|
||||
prismaMock.booking.findFirst.mockResolvedValue(userBooking);
|
||||
prismaMock.booking.findFirst.mockResolvedValue(userBooking as any);
|
||||
|
||||
const attendeeData = {
|
||||
id: 1,
|
||||
|
||||
@@ -25,7 +25,7 @@ beforeEach(() => {
|
||||
id: userId,
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
});
|
||||
} as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -20,7 +20,7 @@ beforeEach(() => {
|
||||
id: userId,
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
});
|
||||
} as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -30,12 +30,13 @@ beforeEach(() => {
|
||||
id: userId,
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
});
|
||||
} as any);
|
||||
(getAccessibleUsers as any).mockResolvedValue([userId]);
|
||||
(retrieveOrgScopedAccessibleUsers as any).mockResolvedValue([userId]);
|
||||
|
||||
prismaMock.membership.findMany.mockResolvedValue([
|
||||
{
|
||||
// @ts-expect-error Will be fixed by Prisma 6.7.0 upgrade mock changes - which uses vitest-mock-extended
|
||||
team: {
|
||||
id: 1,
|
||||
isOrganization: true,
|
||||
@@ -48,7 +49,7 @@ beforeEach(() => {
|
||||
id: userId,
|
||||
email: "test@example.com",
|
||||
},
|
||||
]);
|
||||
] as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -11,9 +11,9 @@ import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBook
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { buildBooking, buildEventType, buildWebhook, buildUser } from "@calcom/lib/test/builder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Booking } from "@calcom/prisma/client";
|
||||
import { CreationSource } from "@calcom/prisma/enums";
|
||||
import { CreationSource, BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import handler from "../../../pages/api/bookings/_post";
|
||||
|
||||
@@ -303,6 +303,7 @@ describe("POST /api/bookings", () => {
|
||||
|
||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
|
||||
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||
profile: { organizationId: null },
|
||||
hosts: [],
|
||||
users: [buildUser()],
|
||||
@@ -340,6 +341,7 @@ describe("POST /api/bookings", () => {
|
||||
|
||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
|
||||
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||
profile: { organizationId: null },
|
||||
hosts: [],
|
||||
users: [buildUser()],
|
||||
@@ -376,6 +378,7 @@ describe("POST /api/bookings", () => {
|
||||
|
||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||
...buildEventType({ profileId: null }),
|
||||
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||
profile: { organizationId: null },
|
||||
hosts: [],
|
||||
users: [buildUser()],
|
||||
@@ -417,6 +420,7 @@ describe("POST /api/bookings", () => {
|
||||
|
||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||
...buildEventType({ profileId: null, length: 15 }),
|
||||
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||
profile: { organizationId: null },
|
||||
hosts: [],
|
||||
users: [buildUser()],
|
||||
@@ -468,7 +472,7 @@ describe("POST /api/bookings", () => {
|
||||
|
||||
prismaMock.booking.findUnique.mockResolvedValue({
|
||||
...originalBooking,
|
||||
status: "cancelled",
|
||||
status: "CANCELLED",
|
||||
});
|
||||
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
@@ -492,6 +496,7 @@ describe("POST /api/bookings", () => {
|
||||
|
||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||
...buildEventType({ profileId: null, length: 15 }),
|
||||
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||
profile: { organizationId: null },
|
||||
hosts: [],
|
||||
users: [buildUser()],
|
||||
@@ -532,7 +537,7 @@ describe("POST /api/bookings", () => {
|
||||
const previousBooking = await prisma.booking.findUnique({
|
||||
where: { uid: "original-booking-uid" },
|
||||
});
|
||||
expect(previousBooking?.status).toBe("cancelled");
|
||||
expect(previousBooking?.status).toBe(BookingStatus.CANCELLED);
|
||||
});
|
||||
|
||||
test("Creates source as api_v1", async () => {
|
||||
@@ -556,6 +561,7 @@ describe("POST /api/bookings", () => {
|
||||
|
||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||
...buildEventType({ profileId: null, length: 15 }),
|
||||
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||
profile: { organizationId: null },
|
||||
hosts: [],
|
||||
users: [buildUser()],
|
||||
@@ -627,6 +633,7 @@ describe("POST /api/bookings", () => {
|
||||
|
||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||
...buildEventType({ profileId: null, length: 15 }),
|
||||
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||
profile: { organizationId: null },
|
||||
hosts: [],
|
||||
users: [buildUser()],
|
||||
@@ -684,6 +691,7 @@ describe("POST /api/bookings", () => {
|
||||
|
||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
|
||||
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||
profile: { organizationId: null },
|
||||
hosts: [],
|
||||
users: [buildUser()],
|
||||
|
||||
@@ -50,6 +50,7 @@ describe("DELETE /api/event-types/[id]", () => {
|
||||
// Mocking team.findUnique
|
||||
prismaMock.team.findUnique.mockResolvedValue({
|
||||
id: teamId,
|
||||
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||
members: [
|
||||
{ userId: memberUser, role: MembershipRole.MEMBER, teamId: teamId },
|
||||
{ userId: adminUser, role: MembershipRole.ADMIN, teamId: teamId },
|
||||
|
||||
@@ -84,6 +84,7 @@ describe("GET /api/event-types/[id]", () => {
|
||||
|
||||
prismaMock.team.findFirst.mockResolvedValue({
|
||||
id: teamId,
|
||||
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
|
||||
members: [
|
||||
{
|
||||
userId,
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.335",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.350",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
@@ -55,9 +55,9 @@
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/throttler": "6.2.1",
|
||||
"@sentry/nestjs": "^8.37.1",
|
||||
"@sentry/node": "^8.8.0",
|
||||
"@sentry/profiling-node": "^8.37.1",
|
||||
"@sentry/nestjs": "^9.15.0",
|
||||
"@sentry/node": "^9.15.0",
|
||||
"@sentry/profiling-node": "^9.15.0",
|
||||
"@snyk/protect": "latest",
|
||||
"body-parser": "^1.20.2",
|
||||
"bull": "^4.12.4",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
@@ -41,6 +42,7 @@ import { Module } from "@nestjs/common";
|
||||
OAuthClientUsersService,
|
||||
PlatformBookingsService,
|
||||
CalendarsService,
|
||||
CalendarsCacheService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ApiQuery, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||
import { CreationSource } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { NextApiRequest } from "next/types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
@@ -58,6 +57,7 @@ import {
|
||||
getBookingForReschedule,
|
||||
ErrorCode,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { CreationSource } from "@calcom/platform-libraries";
|
||||
import {
|
||||
GetBookingsInput_2024_04_15,
|
||||
CancelBookingInput_2024_04_15,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08
|
||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
@@ -71,6 +72,7 @@ import { Module } from "@nestjs/common";
|
||||
ApiKeysRepository,
|
||||
PlatformBookingsService,
|
||||
CalendarsService,
|
||||
CalendarsCacheService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
|
||||
@@ -117,6 +117,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
slug: seatedEventTypeSlug,
|
||||
length: 60,
|
||||
seatsPerTimeSlot: 3,
|
||||
seatsShowAttendees: true,
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
@@ -53,8 +53,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
let apiKeyString: string;
|
||||
|
||||
let seatedEventTypeId: number;
|
||||
let seatedEventTypeIdAttendeesDisabledId: number;
|
||||
|
||||
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
|
||||
const seatedEventSlugAttendeesDisabled = `seated-bookings-event-type-attendees-disabled-${randomString()}`;
|
||||
|
||||
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
|
||||
let createdSeatedBooking2: CreateSeatedBookingOutput_2024_08_13;
|
||||
@@ -121,6 +123,28 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
);
|
||||
seatedEventTypeId = seatedEvent.id;
|
||||
|
||||
const seatedEventAttendeesDisabled = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `seated-bookings-2024-08-13-event-type-attendees-disabled-${randomString()}`,
|
||||
slug: seatedEventSlugAttendeesDisabled,
|
||||
length: 60,
|
||||
seatsPerTimeSlot: 5,
|
||||
seatsShowAttendees: false,
|
||||
seatsShowAvailabilityCount: false,
|
||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||
metadata: {
|
||||
disableStandardEmails: {
|
||||
all: {
|
||||
attendee: true,
|
||||
host: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user.id
|
||||
);
|
||||
seatedEventTypeIdAttendeesDisabledId = seatedEventAttendeesDisabled.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
@@ -414,6 +438,63 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should book an event type with attendees disabled", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
|
||||
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
|
||||
attendee: {
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.seatUid).toBeDefined();
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeIdAttendeesDisabledId,
|
||||
slug: seatedEventSlugAttendeesDisabled,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(0);
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected seated booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancel seated booking", () => {
|
||||
describe("cancel seated booking as attendee", () => {
|
||||
it("should cancel seated booking", async () => {
|
||||
|
||||
@@ -576,10 +576,13 @@ export class BookingsService_2024_08_13 {
|
||||
return this.outputService.getOutputRecurringBooking(booking);
|
||||
}
|
||||
if (isRecurring && isSeated) {
|
||||
return this.outputService.getOutputRecurringSeatedBooking(booking);
|
||||
return this.outputService.getOutputRecurringSeatedBooking(
|
||||
booking,
|
||||
!!booking.eventType?.seatsShowAttendees
|
||||
);
|
||||
}
|
||||
if (isSeated) {
|
||||
return this.outputService.getOutputSeatedBooking(booking);
|
||||
return this.outputService.getOutputSeatedBooking(booking, !!booking.eventType?.seatsShowAttendees);
|
||||
}
|
||||
return this.outputService.getOutputBooking(booking);
|
||||
}
|
||||
@@ -591,7 +594,10 @@ export class BookingsService_2024_08_13 {
|
||||
const ids = recurringBooking.map((booking) => booking.id);
|
||||
const isRecurringSeated = !!recurringBooking[0].eventType?.seatsPerTimeSlot;
|
||||
if (isRecurringSeated) {
|
||||
return this.outputService.getOutputRecurringSeatedBookings(ids);
|
||||
return this.outputService.getOutputRecurringSeatedBookings(
|
||||
ids,
|
||||
!!recurringBooking[0].eventType?.seatsShowAttendees
|
||||
);
|
||||
}
|
||||
|
||||
return this.outputService.getOutputRecurringBookings(ids);
|
||||
@@ -657,9 +663,19 @@ export class BookingsService_2024_08_13 {
|
||||
if (isRecurring && !isSeated) {
|
||||
formattedBookings.push(this.outputService.getOutputRecurringBooking(formatted));
|
||||
} else if (isRecurring && isSeated) {
|
||||
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted));
|
||||
formattedBookings.push(
|
||||
this.outputService.getOutputRecurringSeatedBooking(
|
||||
formatted,
|
||||
!!formatted.eventType?.seatsShowAttendees
|
||||
)
|
||||
);
|
||||
} else if (isSeated) {
|
||||
formattedBookings.push(await this.outputService.getOutputSeatedBooking(formatted));
|
||||
formattedBookings.push(
|
||||
await this.outputService.getOutputSeatedBooking(
|
||||
formatted,
|
||||
!!formatted.eventType?.seatsShowAttendees
|
||||
)
|
||||
);
|
||||
} else {
|
||||
formattedBookings.push(await this.outputService.getOutputBooking(formatted));
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { CreationSource } from "@prisma/client";
|
||||
import { isURL, isPhoneNumber } from "class-validator";
|
||||
import { Request } from "express";
|
||||
import { DateTime } from "luxon";
|
||||
@@ -30,6 +29,7 @@ import { NextApiRequest } from "next/types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CreationSource } from "@calcom/platform-libraries";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
|
||||
@@ -68,6 +68,7 @@ type DatabaseBooking = Booking & {
|
||||
eventType: {
|
||||
id: number;
|
||||
slug: string;
|
||||
seatsShowAttendees?: boolean | null;
|
||||
} | null;
|
||||
attendees: {
|
||||
name: string;
|
||||
@@ -167,6 +168,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
||||
if (databaseMetadata === null) return {};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { videoCallUrl, ...userDefinedMetadata } = databaseMetadata;
|
||||
|
||||
return userDefinedMetadata;
|
||||
@@ -266,11 +268,14 @@ export class OutputBookingsService_2024_08_13 {
|
||||
databaseBooking: DatabaseBooking,
|
||||
seatUid: string
|
||||
): Promise<CreateSeatedBookingOutput_2024_08_13> {
|
||||
const getSeatedBookingOutput = await this.getOutputSeatedBooking(databaseBooking);
|
||||
const getSeatedBookingOutput = await this.getOutputSeatedBooking(
|
||||
databaseBooking,
|
||||
!!databaseBooking.eventType?.seatsShowAttendees
|
||||
);
|
||||
return { ...getSeatedBookingOutput, seatUid };
|
||||
}
|
||||
|
||||
async getOutputSeatedBooking(databaseBooking: DatabaseBooking) {
|
||||
async getOutputSeatedBooking(databaseBooking: DatabaseBooking, showAttendees: boolean) {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
@@ -317,41 +322,43 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
|
||||
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
||||
parsed.attendees = databaseBooking.attendees.map((attendee) => {
|
||||
const { responses } = safeParse(
|
||||
seatedBookingDataSchema,
|
||||
attendee.bookingSeat?.data,
|
||||
defaultSeatedBookingData,
|
||||
false
|
||||
);
|
||||
parsed.attendees = showAttendees
|
||||
? databaseBooking.attendees.map((attendee) => {
|
||||
const { responses } = safeParse(
|
||||
seatedBookingDataSchema,
|
||||
attendee.bookingSeat?.data,
|
||||
defaultSeatedBookingData,
|
||||
false
|
||||
);
|
||||
|
||||
const attendeeData = {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: !!attendee.noShow,
|
||||
seatUid: attendee.bookingSeat?.referenceUid,
|
||||
bookingFieldsResponses: {},
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
attendeeParsed.metadata = safeParse(
|
||||
seatedBookingMetadataSchema,
|
||||
attendee.bookingSeat?.metadata,
|
||||
defaultSeatedBookingMetadata,
|
||||
false
|
||||
);
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
const attendeeData = {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: !!attendee.noShow,
|
||||
seatUid: attendee.bookingSeat?.referenceUid,
|
||||
bookingFieldsResponses: {},
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
attendeeParsed.metadata = safeParse(
|
||||
seatedBookingMetadataSchema,
|
||||
attendee.bookingSeat?.metadata,
|
||||
defaultSeatedBookingMetadata,
|
||||
false
|
||||
);
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
|
||||
return attendeeParsed;
|
||||
});
|
||||
return attendeeParsed;
|
||||
})
|
||||
: [];
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async getOutputRecurringSeatedBookings(bookingsIds: number[]) {
|
||||
async getOutputRecurringSeatedBookings(bookingsIds: number[], showAttendees: boolean) {
|
||||
const transformed = [];
|
||||
|
||||
for (const bookingId of bookingsIds) {
|
||||
@@ -361,7 +368,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
throw new Error(`Booking with id=${bookingId} was not found in the database`);
|
||||
}
|
||||
|
||||
transformed.push(this.getOutputRecurringSeatedBooking(databaseBooking));
|
||||
transformed.push(this.getOutputRecurringSeatedBooking(databaseBooking, showAttendees));
|
||||
}
|
||||
|
||||
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||
@@ -386,11 +393,14 @@ export class OutputBookingsService_2024_08_13 {
|
||||
databaseBooking: DatabaseBooking,
|
||||
seatUid: string
|
||||
): CreateRecurringSeatedBookingOutput_2024_08_13 {
|
||||
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(databaseBooking);
|
||||
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(
|
||||
databaseBooking,
|
||||
!!databaseBooking.eventType?.seatsShowAttendees
|
||||
);
|
||||
return { ...getRecurringSeatedBookingOutput, seatUid };
|
||||
}
|
||||
|
||||
getOutputRecurringSeatedBooking(databaseBooking: DatabaseBooking) {
|
||||
getOutputRecurringSeatedBooking(databaseBooking: DatabaseBooking, showAttendees: boolean) {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
@@ -430,35 +440,37 @@ export class OutputBookingsService_2024_08_13 {
|
||||
});
|
||||
|
||||
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
||||
parsed.attendees = databaseBooking.attendees.map((attendee) => {
|
||||
const { responses } = safeParse(
|
||||
seatedBookingDataSchema,
|
||||
attendee.bookingSeat?.data,
|
||||
defaultSeatedBookingData,
|
||||
false
|
||||
);
|
||||
parsed.attendees = showAttendees
|
||||
? databaseBooking.attendees.map((attendee) => {
|
||||
const { responses } = safeParse(
|
||||
seatedBookingDataSchema,
|
||||
attendee.bookingSeat?.data,
|
||||
defaultSeatedBookingData,
|
||||
false
|
||||
);
|
||||
|
||||
const attendeeData = {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: !!attendee.noShow,
|
||||
seatUid: attendee.bookingSeat?.referenceUid,
|
||||
bookingFieldsResponses: {},
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
attendeeParsed.metadata = safeParse(
|
||||
seatedBookingMetadataSchema,
|
||||
attendee.bookingSeat?.metadata,
|
||||
defaultSeatedBookingMetadata,
|
||||
false
|
||||
);
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
return attendeeParsed;
|
||||
});
|
||||
const attendeeData = {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: !!attendee.noShow,
|
||||
seatUid: attendee.bookingSeat?.referenceUid,
|
||||
bookingFieldsResponses: {},
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
attendeeParsed.metadata = safeParse(
|
||||
seatedBookingMetadataSchema,
|
||||
attendee.bookingSeat?.metadata,
|
||||
defaultSeatedBookingMetadata,
|
||||
false
|
||||
);
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
return attendeeParsed;
|
||||
})
|
||||
: [];
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
|
||||
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
|
||||
import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service";
|
||||
@@ -10,16 +11,18 @@ import { OutlookService } from "@/ee/calendars/services/outlook.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, UsersModule, TokensModule],
|
||||
imports: [PrismaModule, UsersModule, TokensModule, RedisModule],
|
||||
providers: [
|
||||
CredentialsRepository,
|
||||
CalendarsService,
|
||||
CalendarsCacheService,
|
||||
OutlookService,
|
||||
GoogleCalendarService,
|
||||
AppleCalendarService,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
DeletedCalendarCredentialsOutputDto,
|
||||
} from "@/ee/calendars/outputs/delete-calendar-credentials.output";
|
||||
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
|
||||
import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service";
|
||||
@@ -79,6 +80,7 @@ const calendarStateSchema = z.object({
|
||||
export class CalendarsController {
|
||||
constructor(
|
||||
private readonly calendarsService: CalendarsService,
|
||||
private readonly calendarsCacheService: CalendarsCacheService,
|
||||
private readonly outlookService: OutlookService,
|
||||
private readonly googleCalendarService: GoogleCalendarService,
|
||||
private readonly appleCalendarService: AppleCalendarService,
|
||||
@@ -307,6 +309,8 @@ export class CalendarsController {
|
||||
credentialId
|
||||
);
|
||||
|
||||
this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(user.id);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import type { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/platform-libraries";
|
||||
|
||||
export const REDIS_CALENDARS_CACHE_KEY = (userId: number) => `apiv2:user:${userId}:calendars`;
|
||||
export const CALENDARS_CACHE_TTL_MS = 10_000;
|
||||
|
||||
type ConnectedDestinationCalendars = Awaited<
|
||||
ReturnType<typeof getConnectedDestinationCalendarsAndEnsureDefaultsInDb>
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
export class CalendarsCacheService {
|
||||
constructor(private readonly redisService: RedisService) {}
|
||||
|
||||
async deleteConnectedAndDestinationCalendarsCache(userId: number) {
|
||||
await this.redisService.del(REDIS_CALENDARS_CACHE_KEY(userId));
|
||||
}
|
||||
|
||||
async getConnectedAndDestinationCalendarsCache(userId: number) {
|
||||
const cachedResult = await this.redisService.get<ConnectedDestinationCalendars>(
|
||||
REDIS_CALENDARS_CACHE_KEY(userId)
|
||||
);
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
async setConnectedAndDestinationCalendarsCache(
|
||||
userId: number,
|
||||
calendars: ConnectedDestinationCalendars
|
||||
): Promise<void> {
|
||||
await this.redisService.set<ConnectedDestinationCalendars>(REDIS_CALENDARS_CACHE_KEY(userId), calendars, {
|
||||
ttl: CALENDARS_CACHE_TTL_MS,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import {
|
||||
CredentialsRepository,
|
||||
@@ -37,7 +38,8 @@ export class CalendarsService {
|
||||
private readonly appsRepository: AppsRepository,
|
||||
private readonly calendarsRepository: CalendarsRepository,
|
||||
private readonly dbWrite: PrismaWriteService,
|
||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
|
||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||
private readonly calendarsCacheService: CalendarsCacheService
|
||||
) {}
|
||||
|
||||
private buildNonDelegationCredentials<TCredential>(credentials: TCredential[]) {
|
||||
@@ -52,11 +54,17 @@ export class CalendarsService {
|
||||
}
|
||||
|
||||
async getCalendars(userId: number) {
|
||||
const cachedResult = await this.calendarsCacheService.getConnectedAndDestinationCalendarsCache(userId);
|
||||
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const userWithCalendars = await this.usersRepository.findByIdWithCalendars(userId);
|
||||
if (!userWithCalendars) {
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
return getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
|
||||
const result = await getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
|
||||
user: {
|
||||
...userWithCalendars,
|
||||
allSelectedCalendars: userWithCalendars.selectedCalendars,
|
||||
@@ -68,6 +76,10 @@ export class CalendarsService {
|
||||
eventTypeId: null,
|
||||
prisma: this.dbWrite.prisma as unknown as PrismaClient,
|
||||
});
|
||||
console.log("saving cache", JSON.stringify(result));
|
||||
await this.calendarsCacheService.setConnectedAndDestinationCalendarsCache(userId, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getBusyTimes(
|
||||
@@ -187,6 +199,8 @@ export class CalendarsService {
|
||||
userId,
|
||||
calendarType
|
||||
);
|
||||
|
||||
await this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(userId);
|
||||
}
|
||||
|
||||
async checkCalendarCredentialValidity(userId: number, credentialId: number, type: string) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ICSFeedCalendarApp } from "@/ee/calendars/calendars.interface";
|
||||
import { CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { BadRequestException, UnauthorizedException, Logger } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@@ -13,7 +15,9 @@ import { IcsFeedCalendarService } from "@calcom/platform-libraries/app-store";
|
||||
export class IcsFeedService implements ICSFeedCalendarApp {
|
||||
constructor(
|
||||
private readonly calendarsService: CalendarsService,
|
||||
private readonly credentialRepository: CredentialsRepository
|
||||
private readonly calendarsCacheService: CalendarsCacheService,
|
||||
private readonly credentialRepository: CredentialsRepository,
|
||||
private readonly redisService: RedisService
|
||||
) {}
|
||||
|
||||
private logger = new Logger("IcsFeedService");
|
||||
@@ -58,6 +62,9 @@ export class IcsFeedService implements ICSFeedCalendarApp {
|
||||
data.key,
|
||||
userId
|
||||
);
|
||||
|
||||
await this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(userId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: {
|
||||
|
||||
+216
-41
@@ -12,6 +12,7 @@ import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User, Schedule, EventType } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
NoticeThresholdUnitEnum,
|
||||
FrequencyInput,
|
||||
} from "@calcom/platform-enums";
|
||||
import { SchedulingType } from "@calcom/platform-libraries";
|
||||
import {
|
||||
ApiSuccessResponse,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
@@ -39,7 +41,6 @@ import {
|
||||
NotesDefaultFieldInput_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
const orderBySlug = (a: { slug: string }, b: { slug: string }) => {
|
||||
if (a.slug < b.slug) return -1;
|
||||
@@ -88,11 +89,16 @@ describe("Event types Endpoints", () => {
|
||||
let schedulesRepostoryFixture: SchedulesRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let apiKeyString: string;
|
||||
let apiKeyOrgUser: string;
|
||||
|
||||
const userEmail = `event-types-2024-06-14-user-${randomString()}@api.com`;
|
||||
const falseTestEmail = `event-types-2024-06-14-false-user-${randomString()}@api.com`;
|
||||
const name = `event-types-2024-06-14-user-${randomString()}`;
|
||||
const username = name;
|
||||
let eventType: EventTypeOutput_2024_06_14;
|
||||
let hiddenEventType: EventTypeOutput_2024_06_14;
|
||||
let user: User;
|
||||
let orgUser: User;
|
||||
let falseTestUser: User;
|
||||
@@ -165,13 +171,10 @@ describe("Event types Endpoints", () => {
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
|
||||
})
|
||||
)
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
|
||||
})
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
@@ -198,12 +201,20 @@ describe("Event types Endpoints", () => {
|
||||
name,
|
||||
username,
|
||||
});
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
|
||||
apiKeyString = `cal_test_${keyString}`;
|
||||
|
||||
orgUser = await userRepositoryFixture.create({
|
||||
email: `event-types-2024-06-14-org-user-${randomString()}@example.com`,
|
||||
name: `event-types-2024-06-14-org-user-${randomString()}`,
|
||||
username: `event-types-2024-06-14-org-user-${randomString()}`,
|
||||
});
|
||||
const { keyString: orgUserApiKeyString } = await apiKeysRepositoryFixture.createApiKey(
|
||||
orgUser.id,
|
||||
null
|
||||
);
|
||||
apiKeyOrgUser = `cal_test_${orgUserApiKeyString}`;
|
||||
|
||||
profileRepositoryFixture.create({
|
||||
uid: `usr-${orgUser.id}`,
|
||||
@@ -334,6 +345,7 @@ describe("Event types Endpoints", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(404);
|
||||
});
|
||||
@@ -370,6 +382,7 @@ describe("Event types Endpoints", () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
|
||||
@@ -395,6 +408,7 @@ describe("Event types Endpoints", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
@@ -504,6 +518,7 @@ describe("Event types Endpoints", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
@@ -576,21 +591,52 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a hidden event type", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class hidden",
|
||||
slug: "coding-class-hidden",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
],
|
||||
hidden: true,
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const createdEventType = responseBody.data;
|
||||
expect(createdEventType).toHaveProperty("id");
|
||||
expect(createdEventType.title).toEqual(body.title);
|
||||
expect(createdEventType.hidden).toEqual(body.hidden);
|
||||
expect(createdEventType.ownerId).toEqual(user.id);
|
||||
hiddenEventType = responseBody.data;
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${username}`)
|
||||
.get(`/api/v2/event-types?username=${user.username}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(1);
|
||||
expect(responseBody.data?.length).toEqual(2);
|
||||
|
||||
const fetchedEventType = responseBody.data?.[0];
|
||||
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
|
||||
const fetchedHiddenEventType = responseBody.data?.find((et) => et.id === hiddenEventType.id);
|
||||
|
||||
expect(fetchedEventType?.id).toEqual(eventType.id);
|
||||
expect(fetchedEventType?.title).toEqual(eventType.title);
|
||||
@@ -602,33 +648,149 @@ describe("Event types Endpoints", () => {
|
||||
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
|
||||
);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
||||
expect(fetchedEventType.bookerLayouts).toEqual(eventType.bookerLayouts);
|
||||
expect(fetchedEventType.confirmationPolicy).toEqual(eventType.confirmationPolicy);
|
||||
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
|
||||
expect(fetchedEventType.customName).toEqual(eventType.customName);
|
||||
expect(fetchedEventType.requiresBookerEmailVerification).toEqual(
|
||||
expect(fetchedEventType?.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||
expect(fetchedEventType?.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||
expect(fetchedEventType?.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||
expect(fetchedEventType?.offsetStart).toEqual(eventType.offsetStart);
|
||||
expect(fetchedEventType?.bookingWindow).toEqual(eventType.bookingWindow);
|
||||
expect(fetchedEventType?.bookerLayouts).toEqual(eventType.bookerLayouts);
|
||||
expect(fetchedEventType?.confirmationPolicy).toEqual(eventType.confirmationPolicy);
|
||||
expect(fetchedEventType?.recurrence).toEqual(eventType.recurrence);
|
||||
expect(fetchedEventType?.customName).toEqual(eventType.customName);
|
||||
expect(fetchedEventType?.requiresBookerEmailVerification).toEqual(
|
||||
eventType.requiresBookerEmailVerification
|
||||
);
|
||||
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
||||
expect(fetchedEventType.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
|
||||
expect(fetchedEventType.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
|
||||
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||
expect(fetchedEventType?.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
||||
expect(fetchedEventType?.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
|
||||
expect(fetchedEventType?.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
|
||||
expect(fetchedEventType?.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||
eventType.lockTimeZoneToggleOnBookingPage
|
||||
);
|
||||
expect(fetchedEventType.color).toEqual(eventType.color);
|
||||
expect(fetchedEventType?.color).toEqual(eventType.color);
|
||||
expect(fetchedEventType?.hidden).toEqual(false);
|
||||
|
||||
expect(fetchedHiddenEventType?.id).toEqual(hiddenEventType.id);
|
||||
expect(fetchedHiddenEventType?.hidden).toEqual(true);
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username should not return hidden event type if auth of non event type owner provided`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${user.username}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyOrgUser}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(1);
|
||||
|
||||
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
|
||||
|
||||
expect(fetchedEventType?.id).toEqual(eventType.id);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
expect(fetchedEventType?.hidden).toEqual(false);
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username should not return hidden event type if no auth provided`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${user.username}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(1);
|
||||
|
||||
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
|
||||
|
||||
expect(fetchedEventType?.id).toEqual(eventType.id);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
expect(fetchedEventType?.hidden).toEqual(false);
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and eventSlug`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${user.username}&eventSlug=${eventType.slug}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(1);
|
||||
|
||||
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
|
||||
|
||||
expect(fetchedEventType?.id).toEqual(eventType.id);
|
||||
expect(fetchedEventType?.title).toEqual(eventType.title);
|
||||
expect(fetchedEventType?.description).toEqual(eventType.description);
|
||||
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
|
||||
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
|
||||
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
|
||||
);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
expect(fetchedEventType?.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||
expect(fetchedEventType?.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||
expect(fetchedEventType?.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||
expect(fetchedEventType?.offsetStart).toEqual(eventType.offsetStart);
|
||||
expect(fetchedEventType?.bookingWindow).toEqual(eventType.bookingWindow);
|
||||
expect(fetchedEventType?.bookerLayouts).toEqual(eventType.bookerLayouts);
|
||||
expect(fetchedEventType?.confirmationPolicy).toEqual(eventType.confirmationPolicy);
|
||||
expect(fetchedEventType?.recurrence).toEqual(eventType.recurrence);
|
||||
expect(fetchedEventType?.customName).toEqual(eventType.customName);
|
||||
expect(fetchedEventType?.requiresBookerEmailVerification).toEqual(
|
||||
eventType.requiresBookerEmailVerification
|
||||
);
|
||||
expect(fetchedEventType?.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
||||
expect(fetchedEventType?.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
|
||||
expect(fetchedEventType?.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
|
||||
expect(fetchedEventType?.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||
eventType.lockTimeZoneToggleOnBookingPage
|
||||
);
|
||||
expect(fetchedEventType?.color).toEqual(eventType.color);
|
||||
expect(fetchedEventType?.hidden).toEqual(false);
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and eventSlug should not return hidden event type if auth of non event type owner provided`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${user.username}&eventSlug=${hiddenEventType.slug}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyOrgUser}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(0);
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and eventSlug should not return hidden event type if no auth provided`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${user.username}&eventSlug=${hiddenEventType.slug}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(0);
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and orgSlug`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
@@ -647,8 +809,7 @@ describe("Event types Endpoints", () => {
|
||||
`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}&eventSlug=${orgUserEventType1.slug}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
@@ -663,8 +824,7 @@ describe("Event types Endpoints", () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${orgUser.username}&orgId=${organization.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
@@ -705,6 +865,7 @@ describe("Event types Endpoints", () => {
|
||||
await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
@@ -732,6 +893,7 @@ describe("Event types Endpoints", () => {
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(201);
|
||||
|
||||
@@ -748,6 +910,7 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send({
|
||||
seats: {
|
||||
@@ -779,6 +942,7 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
@@ -799,6 +963,7 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(201);
|
||||
@@ -817,6 +982,7 @@ describe("Event types Endpoints", () => {
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send({
|
||||
seats: {
|
||||
seatsPerTimeSlot: 4,
|
||||
@@ -844,6 +1010,7 @@ describe("Event types Endpoints", () => {
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(201);
|
||||
|
||||
@@ -860,6 +1027,7 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send({
|
||||
locations: [
|
||||
@@ -897,6 +1065,7 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
@@ -918,6 +1087,7 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(201);
|
||||
@@ -935,6 +1105,7 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send({
|
||||
confirmationPolicy: {
|
||||
@@ -1034,6 +1205,7 @@ describe("Event types Endpoints", () => {
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${eventType.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
@@ -1135,6 +1307,7 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${eventType.id}`)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(404);
|
||||
@@ -1152,6 +1325,7 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${eventType.id}`)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
@@ -1161,8 +1335,7 @@ describe("Event types Endpoints", () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${eventType.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
@@ -1203,8 +1376,7 @@ describe("Event types Endpoints", () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${username}&eventSlug=${eventType.slug}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
@@ -1245,13 +1417,16 @@ describe("Event types Endpoints", () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/1000`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it("should delete event type", async () => {
|
||||
return request(app.getHttpServer()).delete(`/api/v2/event-types/${eventType.id}`).expect(200);
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/api/v2/event-types/${eventType.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
+28
-24
@@ -6,11 +6,22 @@ import { UpdateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2
|
||||
import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer";
|
||||
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
|
||||
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
||||
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||
import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import {
|
||||
API_KEY_OR_ACCESS_TOKEN_HEADER,
|
||||
OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER,
|
||||
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
|
||||
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
|
||||
} from "@/lib/docs/headers";
|
||||
import {
|
||||
AuthOptionalUser,
|
||||
GetOptionalUser,
|
||||
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
@@ -40,7 +51,6 @@ import {
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
GetEventTypesQuery_2024_06_14,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
@@ -62,7 +72,8 @@ export class EventTypesController_2024_06_14 {
|
||||
constructor(
|
||||
private readonly eventTypesService: EventTypesService_2024_06_14,
|
||||
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
|
||||
private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe
|
||||
private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe,
|
||||
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@@ -109,30 +120,23 @@ export class EventTypesController_2024_06_14 {
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@ApiOperation({ summary: "Get all event types" })
|
||||
@ApiOperation({
|
||||
summary: "Get all event types",
|
||||
description:
|
||||
"Hidden event types are returned only if authentication is provided and it belongs to the event type owner.",
|
||||
})
|
||||
@UseGuards(OptionalApiAuthGuard)
|
||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
async getEventTypes(
|
||||
@Query() queryParams: GetEventTypesQuery_2024_06_14
|
||||
@Query() queryParams: GetEventTypesQuery_2024_06_14,
|
||||
@GetOptionalUser() authUser: AuthOptionalUser
|
||||
): Promise<GetEventTypesOutput_2024_06_14> {
|
||||
const eventTypes = await this.eventTypesService.getEventTypes(queryParams);
|
||||
if (!eventTypes || eventTypes.length === 0) {
|
||||
throw new NotFoundException(`Event types not found`);
|
||||
}
|
||||
const eventTypes = await this.eventTypesService.getEventTypes(queryParams, authUser);
|
||||
const eventTypesFormatted = this.eventTypeResponseTransformPipe.transform(eventTypes);
|
||||
const eventTypesWithoutHiddenFields = eventTypesFormatted.map((eventType) => {
|
||||
return {
|
||||
...eventType,
|
||||
bookingFields: Array.isArray(eventType?.bookingFields)
|
||||
? eventType?.bookingFields
|
||||
.map((field) => {
|
||||
if ("hidden" in field) {
|
||||
return field.hidden !== true ? field : null;
|
||||
}
|
||||
return field;
|
||||
})
|
||||
.filter((f) => f)
|
||||
: [],
|
||||
};
|
||||
}) as EventTypeOutput_2024_06_14[];
|
||||
const eventTypesWithoutHiddenFields =
|
||||
this.outputEventTypesService.getResponseEventTypesWithoutHiddenFields(eventTypesFormatted);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesController_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/controllers/event-types.controller";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
@@ -11,6 +12,7 @@ import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
@@ -18,7 +20,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule],
|
||||
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule, RedisModule],
|
||||
providers: [
|
||||
EventTypesRepository_2024_06_14,
|
||||
EventTypesService_2024_06_14,
|
||||
@@ -29,6 +31,7 @@ import { Module } from "@nestjs/common";
|
||||
SchedulesRepository_2024_06_11,
|
||||
EventTypeResponseTransformPipe,
|
||||
CalendarsService,
|
||||
CalendarsCacheService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
|
||||
@@ -75,6 +75,16 @@ export class EventTypesRepository_2024_06_14 {
|
||||
});
|
||||
}
|
||||
|
||||
async getUserEventTypesPublic(userId: number) {
|
||||
return this.dbRead.prisma.eventType.findMany({
|
||||
where: {
|
||||
userId,
|
||||
hidden: false,
|
||||
},
|
||||
include: { users: true, schedule: true, destinationCalendar: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypeById(eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: { id: eventTypeId },
|
||||
|
||||
+47
-13
@@ -3,6 +3,7 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_20
|
||||
import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed";
|
||||
import { SystemField, CustomField } from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
@@ -38,6 +39,7 @@ export class EventTypesService_2024_06_14 {
|
||||
}
|
||||
await this.checkCanCreateEventType(user.id, body);
|
||||
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { destinationCalendar: _destinationCalendar, ...rest } = body;
|
||||
|
||||
const { eventType: eventTypeCreated } = await createEventType({
|
||||
@@ -93,34 +95,47 @@ export class EventTypesService_2024_06_14 {
|
||||
}
|
||||
}
|
||||
|
||||
async getEventTypeByUsernameAndSlug(
|
||||
username: string,
|
||||
eventTypeSlug: string,
|
||||
orgSlug?: string,
|
||||
orgId?: number
|
||||
) {
|
||||
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
|
||||
async getEventTypeByUsernameAndSlug(params: {
|
||||
username: string;
|
||||
eventTypeSlug: string;
|
||||
orgSlug?: string;
|
||||
orgId?: number;
|
||||
authUser?: AuthOptionalUser;
|
||||
}) {
|
||||
const user = await this.usersRepository.findByUsername(params.username, params.orgSlug, params.orgId);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const eventType = await this.eventTypesRepository.getUserEventTypeBySlug(user.id, eventTypeSlug);
|
||||
const eventType = await this.eventTypesRepository.getUserEventTypeBySlug(user.id, params.eventTypeSlug);
|
||||
|
||||
if (!eventType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (eventType.hidden && params.authUser?.id !== user.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
ownerId: user.id,
|
||||
...eventType,
|
||||
};
|
||||
}
|
||||
|
||||
async getEventTypesByUsername(username: string, orgSlug?: string, orgId?: number) {
|
||||
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
|
||||
async getEventTypesByUsername(params: {
|
||||
username: string;
|
||||
orgSlug?: string;
|
||||
orgId?: number;
|
||||
authUser?: AuthOptionalUser;
|
||||
}) {
|
||||
const user = await this.usersRepository.findByUsername(params.username, params.orgSlug, params.orgId);
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
if (params.authUser?.id !== user.id) {
|
||||
return await this.getUserEventTypesPublic(user.id);
|
||||
}
|
||||
return await this.getUserEventTypes(user.id);
|
||||
}
|
||||
|
||||
@@ -172,6 +187,14 @@ export class EventTypesService_2024_06_14 {
|
||||
});
|
||||
}
|
||||
|
||||
async getUserEventTypesPublic(userId: number) {
|
||||
const eventTypes = await this.eventTypesRepository.getUserEventTypesPublic(userId);
|
||||
|
||||
return eventTypes.map((eventType) => {
|
||||
return { ownerId: userId, ...eventType };
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
|
||||
const user = await this.usersRepository.findByUsername(username);
|
||||
if (!user) {
|
||||
@@ -181,15 +204,26 @@ export class EventTypesService_2024_06_14 {
|
||||
return await getEventTypesPublic(user.id);
|
||||
}
|
||||
|
||||
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14) {
|
||||
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14, authUser?: AuthOptionalUser) {
|
||||
const { username, eventSlug, usernames, orgSlug, orgId } = queryParams;
|
||||
if (username && eventSlug) {
|
||||
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug, orgSlug, orgId);
|
||||
const eventType = await this.getEventTypeByUsernameAndSlug({
|
||||
username,
|
||||
eventTypeSlug: eventSlug,
|
||||
orgSlug,
|
||||
orgId,
|
||||
authUser,
|
||||
});
|
||||
return eventType ? [eventType] : [];
|
||||
}
|
||||
|
||||
if (username) {
|
||||
return await this.getEventTypesByUsername(username, orgSlug, orgId);
|
||||
return await this.getEventTypesByUsername({
|
||||
username,
|
||||
orgSlug,
|
||||
orgId,
|
||||
authUser,
|
||||
});
|
||||
}
|
||||
|
||||
if (usernames) {
|
||||
|
||||
+24
-1
@@ -31,6 +31,7 @@ import {
|
||||
EventTypeOutput_2024_06_14,
|
||||
OutputUnknownLocation_2024_06_14,
|
||||
OutputUnknownBookingField_2024_06_14,
|
||||
OutputBookingField_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
type EventTypeRelations = {
|
||||
@@ -253,7 +254,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
type: "unknown",
|
||||
slug: "unknown",
|
||||
bookingField: JSON.stringify(bookingField),
|
||||
});
|
||||
} satisfies OutputUnknownBookingField_2024_06_14);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,4 +345,26 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
seatsShowAvailabilityCount: !!seatsShowAvailabilityCount,
|
||||
});
|
||||
}
|
||||
|
||||
getResponseEventTypesWithoutHiddenFields(
|
||||
eventTypes: EventTypeOutput_2024_06_14[]
|
||||
): EventTypeOutput_2024_06_14[] {
|
||||
return eventTypes.map((eventType) => this.getResponseEventTypeWithoutHiddenFields(eventType));
|
||||
}
|
||||
|
||||
getResponseEventTypeWithoutHiddenFields(eventType: EventTypeOutput_2024_06_14): EventTypeOutput_2024_06_14 {
|
||||
if (!Array.isArray(eventType?.bookingFields) || eventType.bookingFields.length === 0) return eventType;
|
||||
|
||||
const visibleBookingFields: OutputBookingField_2024_06_14[] = [];
|
||||
for (const bookingField of eventType.bookingFields) {
|
||||
if ("hidden" in bookingField && bookingField.hidden === true) {
|
||||
continue;
|
||||
}
|
||||
visibleBookingFields.push(bookingField);
|
||||
}
|
||||
return {
|
||||
...eventType,
|
||||
bookingFields: visibleBookingFields,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { GcalController } from "@/ee/gcal/gcal.controller";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
@@ -6,6 +7,7 @@ import { GCalService } from "@/modules/apps/services/gcal.service";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
@@ -13,7 +15,7 @@ import { Module } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, TokensModule, OAuthClientModule],
|
||||
imports: [PrismaModule, TokensModule, OAuthClientModule, RedisModule],
|
||||
providers: [
|
||||
AppsRepository,
|
||||
ConfigService,
|
||||
@@ -21,6 +23,7 @@ import { ConfigService } from "@nestjs/config";
|
||||
SelectedCalendarsRepository,
|
||||
GCalService,
|
||||
CalendarsService,
|
||||
CalendarsCacheService,
|
||||
UsersRepository,
|
||||
CalendarsRepository,
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
export const SYSTEM_ADMIN_ROLE = "SYSADMIN";
|
||||
export const ORG_ROLES = [
|
||||
|
||||
@@ -17,6 +17,7 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/inde
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
@@ -36,6 +37,7 @@ import { Module } from "@nestjs/common";
|
||||
SchedulesAtomsService,
|
||||
VerificationAtomsService,
|
||||
RedisService,
|
||||
TeamsRepository,
|
||||
],
|
||||
exports: [EventTypesAtomService],
|
||||
controllers: [
|
||||
|
||||
@@ -64,4 +64,47 @@ export class AtomsRepository {
|
||||
|
||||
return userTeams;
|
||||
}
|
||||
|
||||
async getSecondaryEmails(userId: number) {
|
||||
return await this.dbRead.prisma.secondaryEmail.findMany({
|
||||
where: {
|
||||
userId,
|
||||
emailVerified: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getExistingSecondaryEmailByUserAndEmail(userId: number, email: string) {
|
||||
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
|
||||
where: {
|
||||
userId_email: { userId, email },
|
||||
},
|
||||
});
|
||||
|
||||
return existingSecondaryEmailRecord?.email;
|
||||
}
|
||||
|
||||
async getExistingSecondaryEmail(email: string) {
|
||||
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
return existingSecondaryEmailRecord?.email;
|
||||
}
|
||||
|
||||
async addSecondaryEmail(userId: number, email: string) {
|
||||
const existingSecondaryEmailRecord = await this.dbWrite.prisma.secondaryEmail.create({
|
||||
data: {
|
||||
userId,
|
||||
email,
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return existingSecondaryEmailRecord?.email;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||
import { AddVerifiedEmailInput } from "@/modules/atoms/inputs/add-verified-email.input";
|
||||
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
||||
import { GetVerifiedEmailsParams } from "@/modules/atoms/inputs/get-verified-emails-params";
|
||||
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
||||
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
||||
import { GetVerifiedEmailsOutput } from "@/modules/atoms/outputs/get-verified-emails-output";
|
||||
import { SendVerificationEmailOutput } from "@/modules/atoms/outputs/send-verification-email.output";
|
||||
import { VerifyEmailCodeOutput } from "@/modules/atoms/outputs/verify-email-code.output";
|
||||
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
|
||||
@@ -105,4 +108,44 @@ export class AtomsVerificationController {
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/emails/verified-emails")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async getVerifiedEmails(
|
||||
@Query() query: GetVerifiedEmailsParams,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<GetVerifiedEmailsOutput> {
|
||||
const verifiedEmails = await this.verificationService.getVerifiedEmails({
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
teamId: query.teamId ? Number(query.teamId) : undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
data: verifiedEmails,
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/emails/verified-emails")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async addVerifiedEmails(
|
||||
@Body() body: AddVerifiedEmailInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ApiResponse<{ emailVerified: boolean }>> {
|
||||
const emailVerified = await this.verificationService.addVerifiedEmail({
|
||||
userId: user.id,
|
||||
existingPrimaryEmail: user.email,
|
||||
email: body.email,
|
||||
});
|
||||
|
||||
return {
|
||||
data: { emailVerified },
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEmail } from "class-validator";
|
||||
|
||||
export class AddVerifiedEmailInput {
|
||||
@ApiProperty({ example: "user@example.com" })
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsNumber, IsOptional, IsString, IsEmail } from "class-validator";
|
||||
|
||||
export class GetVerifiedEmailsParams {
|
||||
@ApiPropertyOptional({ example: "12345" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
teamId?: string;
|
||||
}
|
||||
|
||||
export class GetVerifiedEmailsInput {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
userId!: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
@IsString()
|
||||
userEmail!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
teamId?: number;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsString, IsArray } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class GetVerifiedEmailsOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiProperty({ type: [String] })
|
||||
readonly data!: string[];
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
||||
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
||||
import { GetVerifiedEmailsInput } from "@/modules/atoms/inputs/get-verified-emails-params";
|
||||
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
||||
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||
|
||||
@@ -13,6 +16,11 @@ import {
|
||||
|
||||
@Injectable()
|
||||
export class VerificationAtomsService {
|
||||
constructor(
|
||||
private readonly atomsRepository: AtomsRepository,
|
||||
private readonly teamsRepository: TeamsRepository
|
||||
) {}
|
||||
|
||||
async checkEmailVerificationRequired(input: CheckEmailVerificationRequiredParams) {
|
||||
return await checkEmailVerificationRequired(input);
|
||||
}
|
||||
@@ -61,4 +69,78 @@ export class VerificationAtomsService {
|
||||
isVerifyingEmail: input.isVerifyingEmail,
|
||||
});
|
||||
}
|
||||
|
||||
async getVerifiedEmails(input: GetVerifiedEmailsInput): Promise<string[]> {
|
||||
const { userId, userEmail, teamId } = input;
|
||||
const userEmailWithoutOauthClientId = this.removeClientIdFromEmail(userEmail);
|
||||
|
||||
if (teamId) {
|
||||
const verifiedEmails: string[] = [];
|
||||
const teamMembers = await this.teamsRepository.getTeamMemberEmails(teamId);
|
||||
|
||||
if (teamMembers.length === 0) {
|
||||
return verifiedEmails;
|
||||
}
|
||||
|
||||
teamMembers.forEach((member) => {
|
||||
const memberEmailWithoutOauthClientId = this.removeClientIdFromEmail(member.email);
|
||||
|
||||
verifiedEmails.push(memberEmailWithoutOauthClientId);
|
||||
member.secondaryEmails.forEach((secondaryEmail) => {
|
||||
verifiedEmails.push(this.removeClientIdFromEmail(secondaryEmail.email));
|
||||
});
|
||||
});
|
||||
|
||||
return verifiedEmails;
|
||||
}
|
||||
|
||||
let verifiedEmails = [userEmailWithoutOauthClientId];
|
||||
|
||||
const secondaryEmails = await this.atomsRepository.getSecondaryEmails(userId);
|
||||
verifiedEmails = verifiedEmails.concat(
|
||||
secondaryEmails.map((secondaryEmail) => this.removeClientIdFromEmail(secondaryEmail.email))
|
||||
);
|
||||
|
||||
return verifiedEmails;
|
||||
}
|
||||
|
||||
async addVerifiedEmail({
|
||||
userId,
|
||||
existingPrimaryEmail,
|
||||
email,
|
||||
}: {
|
||||
userId: number;
|
||||
existingPrimaryEmail: string;
|
||||
email: string;
|
||||
}): Promise<boolean> {
|
||||
const existingSecondaryEmail = await this.atomsRepository.getExistingSecondaryEmailByUserAndEmail(
|
||||
userId,
|
||||
email
|
||||
);
|
||||
const alreadyExistingEmail = await this.atomsRepository.getExistingSecondaryEmail(email);
|
||||
|
||||
if (alreadyExistingEmail) {
|
||||
throw new BadRequestException("Email already exists");
|
||||
}
|
||||
|
||||
if (existingPrimaryEmail === email || existingSecondaryEmail === email) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await this.atomsRepository.addSecondaryEmail(userId, email);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
removeClientIdFromEmail(email: string): string {
|
||||
const [localPart, domain] = email.split("@");
|
||||
const localPartSegments = localPart.split("+");
|
||||
|
||||
localPartSegments.pop();
|
||||
|
||||
const baseEmail = localPartSegments.join("+");
|
||||
const normalizedEmail = `${baseEmail}@${domain}`;
|
||||
|
||||
return normalizedEmail;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
export const MembershipRoles = Reflector.createDecorator<MembershipRole[]>();
|
||||
|
||||
@@ -6,7 +6,7 @@ import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationRolesGuard implements CanActivate {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { GoogleCalendarService as GCalService } from "@/ee/calendars/services/gcal.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
@@ -8,13 +9,14 @@ import { GoogleCalendarService } from "@/modules/cal-unified-calendars/services/
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [TokensModule],
|
||||
imports: [TokensModule, RedisModule],
|
||||
providers: [
|
||||
GCalService,
|
||||
GoogleCalendarService,
|
||||
@@ -22,6 +24,7 @@ import { Module } from "@nestjs/common";
|
||||
BookingReferencesRepository_2024_08_13,
|
||||
CredentialsRepository,
|
||||
CalendarsService,
|
||||
CalendarsCacheService,
|
||||
SelectedCalendarsRepository,
|
||||
PrismaReadService,
|
||||
PrismaWriteService,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
@@ -6,15 +7,17 @@ import { DestinationCalendarsController } from "@/modules/destination-calendars/
|
||||
import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository";
|
||||
import { DestinationCalendarsService } from "@/modules/destination-calendars/services/destination-calendars.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaModule, RedisModule],
|
||||
providers: [
|
||||
CalendarsRepository,
|
||||
CalendarsService,
|
||||
CalendarsCacheService,
|
||||
DestinationCalendarsService,
|
||||
DestinationCalendarsRepository,
|
||||
UsersRepository,
|
||||
|
||||
+3
@@ -1,4 +1,5 @@
|
||||
import { ConnectedCalendar, Calendar } from "@/ee/calendars/outputs/connected-calendars.output";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
@@ -7,6 +8,7 @@ import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
export class DestinationCalendarsService {
|
||||
constructor(
|
||||
private readonly calendarsService: CalendarsService,
|
||||
private readonly calendarsCacheService: CalendarsCacheService,
|
||||
private readonly destinationCalendarsRepository: DestinationCalendarsRepository
|
||||
) {}
|
||||
|
||||
@@ -60,6 +62,7 @@ export class DestinationCalendarsService {
|
||||
delegatedCalendar ? delegationCredentialId : undefined
|
||||
);
|
||||
|
||||
this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(userId);
|
||||
return {
|
||||
userId: updatedCalendarUserId,
|
||||
integration: updatedCalendarIntegration,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class MembershipsRepository {
|
||||
|
||||
+2
-4
@@ -1,5 +1,4 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { Locales } from "@/lib/enums/locales";
|
||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
||||
@@ -7,7 +6,6 @@ import { GetManagedUsersInput } from "@/modules/oauth-clients/controllers/oauth-
|
||||
import { CreateManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
|
||||
import { GetManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-user.output";
|
||||
import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output";
|
||||
import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output";
|
||||
import { TOKENS_DOCS } from "@/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller";
|
||||
import { KeysResponseDto } from "@/modules/oauth-clients/controllers/oauth-flow/responses/KeysResponse.dto";
|
||||
import { OAuthClientGuard } from "@/modules/oauth-clients/guards/oauth-client-guard";
|
||||
@@ -34,10 +32,10 @@ import {
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags, ApiHeader } from "@nestjs/swagger";
|
||||
import { User, MembershipRole } from "@prisma/client";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/oauth-clients/:clientId/users",
|
||||
|
||||
+2
-1
@@ -17,10 +17,11 @@ import { OAuthClientWebhooksService } from "@/modules/webhooks/services/oauth-cl
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import { Controller, Post, Body, UseGuards, Get, Param, Query, Delete, Patch } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { Webhook, MembershipRole } from "@prisma/client";
|
||||
import { Webhook } from "@prisma/client";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
import { OAuthClientGuard } from "../../guards/oauth-client-guard";
|
||||
|
||||
+1
-3
@@ -5,12 +5,10 @@ import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-role
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
||||
import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output";
|
||||
import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output";
|
||||
import { CreateOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/CreateOAuthClientResponse.dto";
|
||||
import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto";
|
||||
import { GetOAuthClientsResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientsResponse.dto";
|
||||
import { OAuthClientGuard } from "@/modules/oauth-clients/guards/oauth-client-guard";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersOutputService } from "@/modules/oauth-clients/services/oauth-clients-users-output.service";
|
||||
import { OAuthClientsService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients.service";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
@@ -37,9 +35,9 @@ import {
|
||||
ApiExcludeEndpoint,
|
||||
ApiHeader,
|
||||
} from "@nestjs/swagger";
|
||||
import { User, MembershipRole } from "@prisma/client";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
import { CreateOAuthClientInput, UpdateOAuthClientInput, Pagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
@@ -49,6 +50,7 @@ import { JwtService } from "@nestjs/jwt";
|
||||
TokensRepository,
|
||||
OAuthFlowService,
|
||||
CalendarsService,
|
||||
CalendarsCacheService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
|
||||
@@ -119,6 +119,15 @@ export class OAuthClientRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async getByOrgId(organizationId: number) {
|
||||
return this.dbRead.prisma.platformOAuthClient.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getByEventTypeHosts(eventTypeId: number) {
|
||||
const hostWithUserPlatformClient = await this.dbRead.prisma.host.findFirst({
|
||||
select: {
|
||||
|
||||
@@ -8,9 +8,10 @@ import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-us
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ConflictException, Injectable, Logger } from "@nestjs/common";
|
||||
import { User, CreationSource, PlatformOAuthClient } from "@prisma/client";
|
||||
import { User, PlatformOAuthClient } from "@prisma/client";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
|
||||
import { CreationSource } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientUsersService {
|
||||
|
||||
+2
-1
@@ -1,6 +1,5 @@
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { AttributeType } from "@prisma/client";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
@@ -12,6 +11,8 @@ import {
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
|
||||
import { AttributeType } from "@calcom/platform-libraries";
|
||||
|
||||
export class CreateOrganizationAttributeInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { AttributeType } from "@prisma/client";
|
||||
import { IsBoolean, IsEnum, IsOptional, IsString } from "class-validator";
|
||||
|
||||
import { AttributeType } from "@calcom/platform-libraries";
|
||||
|
||||
export class UpdateOrganizationAttributeInput {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@ import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { SchedulingType, User } from "@prisma/client";
|
||||
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";
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
ConfirmationPolicyEnum,
|
||||
NoticeThresholdUnitEnum,
|
||||
} from "@calcom/platform-enums";
|
||||
import { SchedulingType } from "@calcom/platform-libraries";
|
||||
import {
|
||||
ApiSuccessResponse,
|
||||
CreateTeamEventTypeInput_2024_06_14,
|
||||
|
||||
@@ -7,12 +7,12 @@ import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { SchedulingType } from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateTeamEventTypeInput_2024_06_14,
|
||||
UpdateTeamEventTypeInput_2024_06_14,
|
||||
HostPriority,
|
||||
} from "@calcom/platform-types";
|
||||
import { SchedulingType } from "@calcom/prisma/client";
|
||||
|
||||
export type TransformedCreateTeamEventTypeInput = Awaited<
|
||||
ReturnType<InstanceType<typeof InputOrganizationsEventTypesService>["transformInputCreateTeamEventType"]>
|
||||
|
||||
@@ -2,9 +2,17 @@ import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types
|
||||
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { EventType, User, Schedule, Host, DestinationCalendar, CalVideoSettings } from "@prisma/client";
|
||||
import { SchedulingType, Team } from "@prisma/client";
|
||||
import type {
|
||||
Team,
|
||||
EventType,
|
||||
User,
|
||||
Schedule,
|
||||
Host,
|
||||
DestinationCalendar,
|
||||
CalVideoSettings,
|
||||
} from "@prisma/client";
|
||||
|
||||
import { SchedulingType } from "@calcom/platform-libraries";
|
||||
import { HostPriority, TeamEventTypeResponseHost } from "@calcom/platform-types";
|
||||
|
||||
type EventTypeRelations = {
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsBoolean, IsOptional, IsEnum, IsInt } from "class-validator";
|
||||
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
export class CreateOrgMembershipDto {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsBoolean, IsOptional, IsEnum } from "class-validator";
|
||||
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
export class UpdateOrgMembershipDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
|
||||
+34
-2
@@ -1,17 +1,23 @@
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input";
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
||||
|
||||
export const PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR = `Can't add user to organization - the user is platform managed user but organization is not because organization probably was not created using OAuth credentials.`;
|
||||
export const REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR = `Can't add user to organization - the user is not platform managed user but organization is platform managed. Both have to be created using OAuth credentials.`;
|
||||
export const MANAGED_USER_AND_MANAGED_ORG_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR = `Can't add user to organization - managed user and organization were created using different OAuth clients.`;
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsMembershipService {
|
||||
constructor(
|
||||
private readonly organizationsMembershipRepository: OrganizationsMembershipRepository,
|
||||
private readonly organizationsMembershipOutputService: OrganizationsMembershipOutputService
|
||||
private readonly organizationsMembershipOutputService: OrganizationsMembershipOutputService,
|
||||
private readonly oAuthClientsRepository: OAuthClientRepository
|
||||
) {}
|
||||
|
||||
async getOrgMembership(organizationId: number, membershipId: number) {
|
||||
@@ -95,7 +101,33 @@ export class OrganizationsMembershipService {
|
||||
}
|
||||
|
||||
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
|
||||
await this.canUserBeAddedToOrg(data.userId, organizationId);
|
||||
const membership = await this.organizationsMembershipRepository.createOrgMembership(organizationId, data);
|
||||
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||
}
|
||||
|
||||
async canUserBeAddedToOrg(userId: number, orgId: number) {
|
||||
const [userOAuthClient, orgOAuthClients] = await Promise.all([
|
||||
this.oAuthClientsRepository.getByUserId(userId),
|
||||
this.oAuthClientsRepository.getByOrgId(orgId),
|
||||
]);
|
||||
|
||||
if (!userOAuthClient && orgOAuthClients.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userOAuthClient && orgOAuthClients.some((orgClient) => orgClient.id === userOAuthClient.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!userOAuthClient) {
|
||||
throw new BadRequestException(REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR);
|
||||
}
|
||||
|
||||
if (orgOAuthClients.length === 0) {
|
||||
throw new BadRequestException(PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR);
|
||||
}
|
||||
|
||||
throw new BadRequestException(MANAGED_USER_AND_MANAGED_ORG_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import { ZoomVideoService } from "@/modules/conferencing/services/zoom-video.ser
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { EmailModule } from "@/modules/email/email.module";
|
||||
import { EmailService } from "@/modules/email/email.service";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
|
||||
import { OrganizationsAttributesController } from "@/modules/organizations/attributes/index/controllers/organizations-attributes.controller";
|
||||
@@ -65,6 +66,8 @@ import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
|
||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
@@ -93,6 +96,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsOrganizationsModule,
|
||||
OrganizationsStripeModule,
|
||||
OrganizationsTeamsRoutingFormsModule,
|
||||
MembershipsModule,
|
||||
OrganizationsConferencingModule,
|
||||
EventTypesPrivateLinksModule,
|
||||
],
|
||||
@@ -101,7 +105,6 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsTeamsRepository,
|
||||
OrganizationsService,
|
||||
OrganizationsTeamsService,
|
||||
MembershipsRepository,
|
||||
OrganizationsSchedulesService,
|
||||
OrganizationsUsersRepository,
|
||||
OrganizationsUsersService,
|
||||
@@ -147,6 +150,9 @@ import { Module } from "@nestjs/common";
|
||||
TeamsSchedulesService,
|
||||
SchedulesService_2024_06_11,
|
||||
InputSchedulesService_2024_06_11,
|
||||
TeamsMembershipsService,
|
||||
TeamsMembershipsRepository,
|
||||
OAuthClientRepository,
|
||||
],
|
||||
exports: [
|
||||
OrganizationsService,
|
||||
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
import { OrgTeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
export class OrgMeTeamOutputDto extends OrgTeamOutputDto {
|
||||
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { EmailService } from "@/modules/email/email.service";
|
||||
import {
|
||||
CreateManagedUserData,
|
||||
CreateManagedUserOutput,
|
||||
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
|
||||
import {
|
||||
PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR,
|
||||
REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR,
|
||||
} from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||
import { CreateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/create-organization-team.input";
|
||||
import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import {
|
||||
PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR,
|
||||
REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR,
|
||||
PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR,
|
||||
} from "@/modules/teams/memberships/services/teams-memberships.service";
|
||||
import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
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 { 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";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations Teams Memberships Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
|
||||
let org: Team;
|
||||
let orgOwner: User;
|
||||
let orgUser: User;
|
||||
let orgTeam: Team;
|
||||
let orgApiKey: string;
|
||||
|
||||
let platformOrg: Team;
|
||||
let platformOrgOwner: User;
|
||||
let platformOrgUser: CreateManagedUserData;
|
||||
let platformOrgTeam: Team;
|
||||
let platformOAuthClient: PlatformOAuthClient;
|
||||
let secondPlatformOAuthClient: PlatformOAuthClient;
|
||||
let secondPlatformOrgUser: CreateManagedUserData;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule, TeamsMembershipsModule],
|
||||
}).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
|
||||
const orgName = `organizations-teams-memberships-organization-${randomString()}`;
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: orgName,
|
||||
slug: orgName,
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
const platformOrgName = `organizations-teams-memberships-platform-organization-${randomString()}`;
|
||||
platformOrg = await organizationsRepositoryFixture.create({
|
||||
isPlatform: true,
|
||||
name: platformOrgName,
|
||||
slug: platformOrgName,
|
||||
isOrganization: true,
|
||||
platformBilling: {
|
||||
create: {
|
||||
customerId: "cus_999",
|
||||
plan: "ESSENTIALS",
|
||||
subscriptionId: "sub_999",
|
||||
},
|
||||
},
|
||||
});
|
||||
platformOAuthClient = await createOAuthClient(platformOrg.id);
|
||||
secondPlatformOAuthClient = await createOAuthClient(platformOrg.id);
|
||||
|
||||
orgOwner = await userRepositoryFixture.create({
|
||||
email: `organizations-teams-memberships-org-owner-${randomString()}@api.com`,
|
||||
username: `organizations-teams-memberships-org-owner-${randomString()}`,
|
||||
});
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(orgOwner.id, null);
|
||||
orgApiKey = `cal_test_${keyString}`;
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: orgOwner.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
platformOrgOwner = await userRepositoryFixture.create({
|
||||
email: `platform-org-owner-${randomString()}@api.com`,
|
||||
username: `platform-org-owner-${randomString()}`,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: platformOrgOwner.id } },
|
||||
team: { connect: { id: platformOrg.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd1qwwqeqw-asddsadasd",
|
||||
username: `platform-org-owner-${randomString()}`,
|
||||
organization: { connect: { id: platformOrg.id } },
|
||||
user: {
|
||||
connect: { id: platformOrgOwner.id },
|
||||
},
|
||||
});
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd1qwwqeqw-asddsadasd-2",
|
||||
username: `org-owner-${randomString()}`,
|
||||
organization: { connect: { id: org.id } },
|
||||
user: {
|
||||
connect: { id: orgOwner.id },
|
||||
},
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:4321"],
|
||||
permissions: 1023,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("should create org users", () => {
|
||||
it("should create a new org user", async () => {
|
||||
const newOrgUser: CreateUserInput = {
|
||||
email: `organization-user-${randomString()}@api.com`,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(EmailService.prototype, "sendSignupToOrganizationEmail")
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
|
||||
const { body } = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users`)
|
||||
.send(newOrgUser)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.set("Accept", "application/json");
|
||||
|
||||
const userData = body.data;
|
||||
expect(body.status).toBe(SUCCESS_STATUS);
|
||||
expect(userData.email).toBe(newOrgUser.email);
|
||||
orgUser = userData;
|
||||
});
|
||||
|
||||
it(`should create a new platform org manager user using first oAuth client`, async () => {
|
||||
const managedUserEmail = `platform-organization-manager-user-${randomString()}@api.com`;
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: managedUserEmail,
|
||||
timeZone: "Europe/Berlin",
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
name: "Alice Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
bio: "I am a bio",
|
||||
metadata: {
|
||||
key: "value",
|
||||
},
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${platformOAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone);
|
||||
expect(responseBody.data.user.name).toEqual(requestBody.name);
|
||||
await userConnectedToOAuth(platformOAuthClient.id, responseBody.data.user.email, 1);
|
||||
platformOrgUser = responseBody.data;
|
||||
});
|
||||
|
||||
it(`should create a new platform org manager user using second oAuth client`, async () => {
|
||||
const managedUserEmail = `platform-organization-manager-user-${randomString()}@api.com`;
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: managedUserEmail,
|
||||
timeZone: "Europe/Berlin",
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
name: "Alice Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
bio: "I am a bio",
|
||||
metadata: {
|
||||
key: "value",
|
||||
},
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${secondPlatformOAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", secondPlatformOAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone);
|
||||
expect(responseBody.data.user.name).toEqual(requestBody.name);
|
||||
await userConnectedToOAuth(secondPlatformOAuthClient.id, responseBody.data.user.email, 1);
|
||||
secondPlatformOrgUser = responseBody.data;
|
||||
});
|
||||
});
|
||||
|
||||
describe("should create org teams", () => {
|
||||
it("should create the team for org", async () => {
|
||||
const teamName = `organization team ${randomString()}`;
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams`)
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.send({
|
||||
name: teamName,
|
||||
} satisfies CreateOrgTeamDto)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<Team> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
orgTeam = responseBody.data;
|
||||
expect(orgTeam.name).toEqual(teamName);
|
||||
expect(orgTeam.parentId).toEqual(org.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create the team for platform org", async () => {
|
||||
const teamName = `platform organization team ${randomString()}`;
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${platformOrg.id}/teams`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
name: teamName,
|
||||
} satisfies CreateOrgTeamDto)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<Team> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
platformOrgTeam = responseBody.data;
|
||||
expect(platformOrgTeam.name).toEqual(teamName);
|
||||
expect(platformOrgTeam.parentId).toEqual(platformOrg.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("organization memberships", () => {
|
||||
describe("negative tests", () => {
|
||||
it("should not add managed user to organization", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/memberships`)
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.send({
|
||||
userId: platformOrgUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(400);
|
||||
expect(response.body.error.message).toEqual(PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR);
|
||||
});
|
||||
|
||||
it("should not add managed user to organization team", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`)
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.send({
|
||||
userId: platformOrgUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(422);
|
||||
});
|
||||
|
||||
it("should not add managed user to organization team", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/teams/${orgTeam.id}/memberships`)
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.send({
|
||||
userId: platformOrgUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(400);
|
||||
expect(response.body.error.message).toEqual(PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR);
|
||||
});
|
||||
});
|
||||
|
||||
describe("positive tests", () => {
|
||||
it("should add user to organization", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/memberships`)
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.send({
|
||||
userId: orgUser.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(201);
|
||||
});
|
||||
|
||||
it("should add user to organization team", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`)
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.send({
|
||||
userId: orgUser.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(201);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("platform organization memberships", () => {
|
||||
describe("negative tests", () => {
|
||||
it("should not add user to platform organization", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${platformOrg.id}/memberships`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
userId: orgUser.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(400);
|
||||
expect(response.body.error.message).toEqual(REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR);
|
||||
});
|
||||
|
||||
it("should not add user to platform organization team", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${platformOrg.id}/teams/${platformOrgTeam.id}/memberships`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
userId: orgUser.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(422);
|
||||
});
|
||||
it("should not add user to platform organization team", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/teams/${platformOrgTeam.id}/memberships`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
userId: orgUser.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(400);
|
||||
expect(response.body.error.message).toEqual(REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR);
|
||||
});
|
||||
|
||||
it("should not add user to platform organization team because user is created using different oAuth client", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${platformOrg.id}/teams/${platformOrgTeam.id}/memberships`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
userId: secondPlatformOrgUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(400);
|
||||
expect(response.body.error.message).toEqual(
|
||||
PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("positive tests", () => {
|
||||
it("should add user to organization", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${platformOrg.id}/memberships`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
userId: platformOrgUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(201);
|
||||
});
|
||||
|
||||
it("should add user to organization team", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${platformOrg.id}/teams/${platformOrgTeam.id}/memberships`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
userId: platformOrgUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(201);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function userConnectedToOAuth(oAuthClientId: string, userEmail: string, usersCount: number) {
|
||||
const oAuthUsers = await oauthClientRepositoryFixture.getUsers(oAuthClientId);
|
||||
const newOAuthUser = oAuthUsers?.find((user) => user.email === userEmail);
|
||||
|
||||
expect(oAuthUsers?.length).toEqual(usersCount);
|
||||
expect(newOAuthUser?.email).toEqual(userEmail);
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await teamsRepositoryFixture.delete(orgTeam.id);
|
||||
await organizationsRepositoryFixture.delete(platformOrg.id);
|
||||
await teamsRepositoryFixture.delete(platformOrgTeam.id);
|
||||
await userRepositoryFixture.deleteByEmail(platformOrgUser.user.email);
|
||||
await userRepositoryFixture.deleteByEmail(orgUser.email);
|
||||
await userRepositoryFixture.deleteByEmail(orgOwner.email);
|
||||
await userRepositoryFixture.deleteByEmail(platformOrgOwner.email);
|
||||
await userRepositoryFixture.deleteByEmail(secondPlatformOrgUser.user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsBoolean, IsOptional, IsEnum, IsInt } from "class-validator";
|
||||
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
export class CreateOrgTeamMembershipDto {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsBoolean, IsOptional, IsEnum } from "class-validator";
|
||||
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
export class UpdateOrgTeamMembershipDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
|
||||
+4
-1
@@ -1,6 +1,7 @@
|
||||
import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input";
|
||||
import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/update-organization-team-membership.input";
|
||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
@@ -8,10 +9,12 @@ import { TeamService } from "@calcom/platform-libraries";
|
||||
@Injectable()
|
||||
export class OrganizationsTeamsMembershipsService {
|
||||
constructor(
|
||||
private readonly organizationsTeamsMembershipsRepository: OrganizationsTeamsMembershipsRepository
|
||||
private readonly organizationsTeamsMembershipsRepository: OrganizationsTeamsMembershipsRepository,
|
||||
private readonly teamsMembershipsService: TeamsMembershipsService
|
||||
) {}
|
||||
|
||||
async createOrgTeamMembership(teamId: number, data: CreateOrgTeamMembershipDto) {
|
||||
await this.teamsMembershipsService.canUserBeAddedToTeam(data.userId, teamId);
|
||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.createOrgTeamMembership(
|
||||
teamId,
|
||||
data
|
||||
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsString, IsOptional, IsBoolean, IsEnum } from "class-validator";
|
||||
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
export class CreateOrganizationUserInput extends CreateUserInput {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
+2
-1
@@ -4,10 +4,11 @@ import { UpdateOrganizationUserInput } from "@/modules/organizations/users/index
|
||||
import { OrganizationsUsersRepository } from "@/modules/organizations/users/index/organizations-users.repository";
|
||||
import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
|
||||
import { Injectable, ConflictException, ForbiddenException } from "@nestjs/common";
|
||||
import { Team, CreationSource } from "@prisma/client";
|
||||
import { Team } from "@prisma/client";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries";
|
||||
import { CreationSource } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsUsersService {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
@@ -7,6 +8,7 @@ import { OrganizationsMembershipRepository } from "@/modules/organizations/membe
|
||||
import { OrganizationsMembershipOutputService } from "@/modules/organizations/memberships/services/organizations-membership-output.service";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsController } from "@/modules/selected-calendars/controllers/selected-calendars.controller";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { SelectedCalendarsService } from "@/modules/selected-calendars/services/selected-calendars.service";
|
||||
@@ -14,11 +16,12 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaModule, RedisModule],
|
||||
providers: [
|
||||
SelectedCalendarsRepository,
|
||||
CalendarsRepository,
|
||||
CalendarsService,
|
||||
CalendarsCacheService,
|
||||
UsersRepository,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||
@@ -23,6 +24,7 @@ type SelectedCalendarsInputDelegationCredential = SelectedCalendarsInputDto & {
|
||||
export class SelectedCalendarsService {
|
||||
constructor(
|
||||
private readonly calendarsService: CalendarsService,
|
||||
private readonly calendarsCacheService: CalendarsCacheService,
|
||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||
private readonly organizationsMembershipService: OrganizationsMembershipService,
|
||||
private readonly organizationsDelegationCredentialRepository: OrganizationsDelegationCredentialRepository
|
||||
@@ -50,6 +52,8 @@ export class SelectedCalendarsService {
|
||||
credentialId
|
||||
);
|
||||
|
||||
await this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(user.id);
|
||||
|
||||
return userSelectedCalendar;
|
||||
}
|
||||
|
||||
@@ -122,7 +126,7 @@ export class SelectedCalendarsService {
|
||||
externalId,
|
||||
delegationCredentialId
|
||||
);
|
||||
|
||||
await this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(user.id);
|
||||
return removedCalendarEntry;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsBoolean, IsOptional, IsEnum, IsInt } from "class-validator";
|
||||
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
export class CreateTeamMembershipInput {
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number })
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsOptional, IsEnum } from "class-validator";
|
||||
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
export class UpdateTeamMembershipInput {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { Expose, Transform, Type } from "class-transformer";
|
||||
import { IsBoolean, IsInt, IsObject, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
class MembershipUserOutputDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
||||
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
export const PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR = `Can't add user to team - the user is platform managed user but team is not because team probably was not created using OAuth credentials.`;
|
||||
export const REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR = `Can't add user to team - the user is not platform managed user but team is platform managed. Both have to be created using OAuth credentials.`;
|
||||
export const PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR = `Can't add user to team - managed user and team were created using different OAuth clients.`;
|
||||
|
||||
@Injectable()
|
||||
export class TeamsMembershipsService {
|
||||
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
||||
constructor(
|
||||
private readonly teamsMembershipsRepository: TeamsMembershipsRepository,
|
||||
private readonly oAuthClientsRepository: OAuthClientRepository
|
||||
) {}
|
||||
|
||||
async createTeamMembership(teamId: number, data: CreateTeamMembershipInput) {
|
||||
await this.canUserBeAddedToTeam(data.userId, teamId);
|
||||
const teamMembership = await this.teamsMembershipsRepository.createTeamMembership(teamId, data);
|
||||
return teamMembership;
|
||||
}
|
||||
@@ -54,4 +63,29 @@ export class TeamsMembershipsService {
|
||||
|
||||
return teamMembership;
|
||||
}
|
||||
|
||||
async canUserBeAddedToTeam(userId: number, teamId: number) {
|
||||
const [userOAuthClient, teamOAuthClient] = await Promise.all([
|
||||
this.oAuthClientsRepository.getByUserId(userId),
|
||||
this.oAuthClientsRepository.getByTeamId(teamId),
|
||||
]);
|
||||
|
||||
if (!userOAuthClient && !teamOAuthClient) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userOAuthClient && teamOAuthClient && userOAuthClient.id === teamOAuthClient.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!teamOAuthClient) {
|
||||
throw new BadRequestException(PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR);
|
||||
}
|
||||
|
||||
if (!userOAuthClient) {
|
||||
throw new BadRequestException(REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR);
|
||||
}
|
||||
|
||||
throw new BadRequestException(PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ import { Module } from "@nestjs/common";
|
||||
imports: [PrismaModule, RedisModule, OrganizationsModule, MembershipsModule, TeamsEventTypesModule],
|
||||
providers: [TeamsMembershipsRepository, TeamsMembershipsService],
|
||||
controllers: [TeamsMembershipsController],
|
||||
exports: [TeamsMembershipsService],
|
||||
})
|
||||
export class TeamsMembershipsModule {}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { TeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { TeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { TeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
@@ -120,4 +120,30 @@ export class TeamsRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getTeamMemberEmails(teamId: number) {
|
||||
return this.dbRead.prisma.user.findMany({
|
||||
where: {
|
||||
teams: {
|
||||
some: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
secondaryEmails: {
|
||||
where: {
|
||||
emailVerified: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-us
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import type { Profile, User, Team, Prisma } from "@prisma/client";
|
||||
import { CreationSource } from "@prisma/client";
|
||||
|
||||
import { CreationSource } from "@calcom/platform-libraries";
|
||||
|
||||
export type UserWithProfile = User & {
|
||||
movedToProfile?: (Profile & { organization: Pick<Team, "isPlatform" | "id" | "slug" | "name"> }) | null;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ApiProperty, ApiPropertyOptional, PartialType } from "@nestjs/swagger";
|
||||
import { WebhookTriggerEvents } from "@prisma/client";
|
||||
import { IsArray, IsBoolean, IsEnum, IsOptional, IsString } from "class-validator";
|
||||
|
||||
import { WebhookTriggerEvents } from "@calcom/platform-libraries";
|
||||
|
||||
export class CreateWebhookInputDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { WebhookTriggerEvents } from "@prisma/client";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsBoolean, IsEnum, IsInt, IsString, ValidateNested, IsArray } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { WebhookTriggerEvents } from "@calcom/platform-libraries";
|
||||
|
||||
export class WebhookOutputDto {
|
||||
@IsInt()
|
||||
|
||||
@@ -33,6 +33,11 @@ import {
|
||||
AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||
BaseWorkflowTriggerDto,
|
||||
BEFORE_EVENT,
|
||||
BOOKING_NO_SHOW_UPDATED,
|
||||
BOOKING_PAID,
|
||||
BOOKING_PAYMENT_INITIATED,
|
||||
BOOKING_REJECTED,
|
||||
BOOKING_REQUESTED,
|
||||
EVENT_CANCELLED,
|
||||
NEW_EVENT,
|
||||
OnAfterCalVideoGuestsNoShowTriggerDto,
|
||||
@@ -41,6 +46,11 @@ import {
|
||||
OnBeforeEventTriggerDto,
|
||||
OnCancelTriggerDto,
|
||||
OnCreationTriggerDto,
|
||||
OnNoShowUpdateTriggerDto,
|
||||
OnPaidTriggerDto,
|
||||
OnPaymentInitiatedTriggerDto,
|
||||
OnRejectedTriggerDto,
|
||||
OnRequestedTriggerDto,
|
||||
OnRescheduleTriggerDto,
|
||||
RESCHEDULE_EVENT,
|
||||
} from "./workflow-trigger.input";
|
||||
@@ -73,6 +83,11 @@ export type TriggerDtoType =
|
||||
| OnRescheduleTriggerDto
|
||||
| OnCancelTriggerDto
|
||||
| OnAfterCalVideoGuestsNoShowTriggerDto
|
||||
| OnRejectedTriggerDto
|
||||
| OnRequestedTriggerDto
|
||||
| OnPaymentInitiatedTriggerDto
|
||||
| OnPaidTriggerDto
|
||||
| OnNoShowUpdateTriggerDto
|
||||
| OnAfterCalVideoHostsNoShowTriggerDto;
|
||||
|
||||
@ApiExtraModels(
|
||||
@@ -81,6 +96,11 @@ export type TriggerDtoType =
|
||||
OnCancelTriggerDto,
|
||||
OnCreationTriggerDto,
|
||||
OnRescheduleTriggerDto,
|
||||
OnNoShowUpdateTriggerDto,
|
||||
OnRejectedTriggerDto,
|
||||
OnRequestedTriggerDto,
|
||||
OnPaymentInitiatedTriggerDto,
|
||||
OnPaidTriggerDto,
|
||||
OnAfterCalVideoGuestsNoShowTriggerDto,
|
||||
OnAfterCalVideoHostsNoShowTriggerDto,
|
||||
WorkflowEmailAddressStepDto,
|
||||
@@ -112,6 +132,11 @@ export class CreateWorkflowDto {
|
||||
{ $ref: getSchemaPath(OnRescheduleTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnAfterCalVideoGuestsNoShowTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnAfterCalVideoHostsNoShowTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnRejectedTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnRequestedTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnPaidTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnPaymentInitiatedTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnNoShowUpdateTriggerDto) },
|
||||
],
|
||||
})
|
||||
@ValidateNested()
|
||||
@@ -126,6 +151,11 @@ export class CreateWorkflowDto {
|
||||
{ value: OnRescheduleTriggerDto, name: RESCHEDULE_EVENT },
|
||||
{ value: OnAfterCalVideoGuestsNoShowTriggerDto, name: AFTER_GUESTS_CAL_VIDEO_NO_SHOW },
|
||||
{ value: OnAfterCalVideoHostsNoShowTriggerDto, name: AFTER_HOSTS_CAL_VIDEO_NO_SHOW },
|
||||
{ value: OnRequestedTriggerDto, name: BOOKING_REQUESTED },
|
||||
{ value: OnRejectedTriggerDto, name: BOOKING_REJECTED },
|
||||
{ value: OnPaymentInitiatedTriggerDto, name: BOOKING_PAYMENT_INITIATED },
|
||||
{ value: OnPaidTriggerDto, name: BOOKING_PAID },
|
||||
{ value: OnNoShowUpdateTriggerDto, name: BOOKING_NO_SHOW_UPDATED },
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -135,6 +165,11 @@ export class CreateWorkflowDto {
|
||||
| OnCreationTriggerDto
|
||||
| OnRescheduleTriggerDto
|
||||
| OnCancelTriggerDto
|
||||
| OnRejectedTriggerDto
|
||||
| OnRequestedTriggerDto
|
||||
| OnPaidTriggerDto
|
||||
| OnPaymentInitiatedTriggerDto
|
||||
| OnNoShowUpdateTriggerDto
|
||||
| OnAfterCalVideoGuestsNoShowTriggerDto
|
||||
| OnAfterCalVideoHostsNoShowTriggerDto;
|
||||
|
||||
|
||||
@@ -36,6 +36,16 @@ import {
|
||||
AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||
OnAfterCalVideoHostsNoShowTriggerDto,
|
||||
AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||
OnNoShowUpdateTriggerDto,
|
||||
OnRejectedTriggerDto,
|
||||
OnRequestedTriggerDto,
|
||||
OnPaymentInitiatedTriggerDto,
|
||||
OnPaidTriggerDto,
|
||||
BOOKING_REQUESTED,
|
||||
BOOKING_REJECTED,
|
||||
BOOKING_PAYMENT_INITIATED,
|
||||
BOOKING_PAID,
|
||||
BOOKING_NO_SHOW_UPDATED,
|
||||
} from "./workflow-trigger.input";
|
||||
|
||||
export type UpdateWorkflowStepDto =
|
||||
@@ -119,6 +129,11 @@ export class UpdateWhatsAppAttendeePhoneWorkflowStepDto extends WorkflowPhoneWha
|
||||
OnCancelTriggerDto,
|
||||
OnCreationTriggerDto,
|
||||
OnRescheduleTriggerDto,
|
||||
OnNoShowUpdateTriggerDto,
|
||||
OnRejectedTriggerDto,
|
||||
OnRequestedTriggerDto,
|
||||
OnPaymentInitiatedTriggerDto,
|
||||
OnPaidTriggerDto,
|
||||
OnAfterCalVideoGuestsNoShowTriggerDto,
|
||||
OnAfterCalVideoHostsNoShowTriggerDto,
|
||||
UpdateEmailAddressWorkflowStepDto,
|
||||
@@ -155,6 +170,11 @@ export class UpdateWorkflowDto {
|
||||
{ $ref: getSchemaPath(OnRescheduleTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnAfterCalVideoGuestsNoShowTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnAfterCalVideoHostsNoShowTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnRejectedTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnRequestedTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnPaidTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnPaymentInitiatedTriggerDto) },
|
||||
{ $ref: getSchemaPath(OnNoShowUpdateTriggerDto) },
|
||||
],
|
||||
})
|
||||
@IsOptional()
|
||||
@@ -170,6 +190,11 @@ export class UpdateWorkflowDto {
|
||||
{ value: OnRescheduleTriggerDto, name: RESCHEDULE_EVENT },
|
||||
{ value: OnAfterCalVideoGuestsNoShowTriggerDto, name: AFTER_GUESTS_CAL_VIDEO_NO_SHOW },
|
||||
{ value: OnAfterCalVideoHostsNoShowTriggerDto, name: AFTER_HOSTS_CAL_VIDEO_NO_SHOW },
|
||||
{ value: OnRequestedTriggerDto, name: BOOKING_REQUESTED },
|
||||
{ value: OnRejectedTriggerDto, name: BOOKING_REJECTED },
|
||||
{ value: OnPaymentInitiatedTriggerDto, name: BOOKING_PAYMENT_INITIATED },
|
||||
{ value: OnPaidTriggerDto, name: BOOKING_PAID },
|
||||
{ value: OnNoShowUpdateTriggerDto, name: BOOKING_NO_SHOW_UPDATED },
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -179,6 +204,11 @@ export class UpdateWorkflowDto {
|
||||
| OnCreationTriggerDto
|
||||
| OnRescheduleTriggerDto
|
||||
| OnCancelTriggerDto
|
||||
| OnRejectedTriggerDto
|
||||
| OnRequestedTriggerDto
|
||||
| OnPaidTriggerDto
|
||||
| OnPaymentInitiatedTriggerDto
|
||||
| OnNoShowUpdateTriggerDto
|
||||
| OnAfterCalVideoGuestsNoShowTriggerDto
|
||||
| OnAfterCalVideoHostsNoShowTriggerDto;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { WorkflowActions, WorkflowTemplates } from "@prisma/client";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsNumber, IsBoolean, IsString, ValidateNested, IsIn } from "class-validator";
|
||||
|
||||
import { WorkflowActions, WorkflowTemplates } from "@calcom/platform-libraries";
|
||||
|
||||
export const EMAIL_HOST = "email_host";
|
||||
export const EMAIL_ATTENDEE = "email_attendee";
|
||||
export const EMAIL_ADDRESS = "email_address";
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsIn, IsNumber, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { TimeUnit, WorkflowTriggerEvents } from "@calcom/platform-libraries";
|
||||
|
||||
export const BEFORE_EVENT = "beforeEvent";
|
||||
export const EVENT_CANCELLED = "eventCancelled";
|
||||
export const NEW_EVENT = "newEvent";
|
||||
@@ -10,6 +11,11 @@ export const AFTER_EVENT = "afterEvent";
|
||||
export const RESCHEDULE_EVENT = "rescheduleEvent";
|
||||
export const AFTER_HOSTS_CAL_VIDEO_NO_SHOW = "afterHostsCalVideoNoShow";
|
||||
export const AFTER_GUESTS_CAL_VIDEO_NO_SHOW = "afterGuestsCalVideoNoShow";
|
||||
export const BOOKING_REJECTED = "bookingRejected";
|
||||
export const BOOKING_REQUESTED = "bookingRequested";
|
||||
export const BOOKING_PAYMENT_INITIATED = "bookingPaymentInitiated";
|
||||
export const BOOKING_PAID = "bookingPaid";
|
||||
export const BOOKING_NO_SHOW_UPDATED = "bookingNoShowUpdated";
|
||||
export const WORKFLOW_TRIGGER_TYPES = [
|
||||
BEFORE_EVENT,
|
||||
EVENT_CANCELLED,
|
||||
@@ -18,6 +24,11 @@ export const WORKFLOW_TRIGGER_TYPES = [
|
||||
RESCHEDULE_EVENT,
|
||||
AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||
AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||
BOOKING_REJECTED,
|
||||
BOOKING_REQUESTED,
|
||||
BOOKING_PAYMENT_INITIATED,
|
||||
BOOKING_PAID,
|
||||
BOOKING_NO_SHOW_UPDATED,
|
||||
] as const;
|
||||
|
||||
export const WORKFLOW_TRIGGER_TO_ENUM = {
|
||||
@@ -28,6 +39,11 @@ export const WORKFLOW_TRIGGER_TO_ENUM = {
|
||||
[RESCHEDULE_EVENT]: WorkflowTriggerEvents.RESCHEDULE_EVENT,
|
||||
[AFTER_HOSTS_CAL_VIDEO_NO_SHOW]: WorkflowTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||
[AFTER_GUESTS_CAL_VIDEO_NO_SHOW]: WorkflowTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||
[BOOKING_REJECTED]: WorkflowTriggerEvents.BOOKING_REJECTED,
|
||||
[BOOKING_REQUESTED]: WorkflowTriggerEvents.BOOKING_REQUESTED,
|
||||
[BOOKING_PAYMENT_INITIATED]: WorkflowTriggerEvents.BOOKING_PAYMENT_INITIATED,
|
||||
[BOOKING_NO_SHOW_UPDATED]: WorkflowTriggerEvents.BOOKING_NO_SHOW_UPDATED,
|
||||
[BOOKING_PAID]: WorkflowTriggerEvents.BOOKING_PAID,
|
||||
} as const;
|
||||
|
||||
export const ENUM_TO_WORKFLOW_TRIGGER = {
|
||||
@@ -38,6 +54,11 @@ export const ENUM_TO_WORKFLOW_TRIGGER = {
|
||||
[WorkflowTriggerEvents.RESCHEDULE_EVENT]: RESCHEDULE_EVENT,
|
||||
[WorkflowTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW]: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||
[WorkflowTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW]: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||
[WorkflowTriggerEvents.BOOKING_REJECTED]: BOOKING_REJECTED,
|
||||
[WorkflowTriggerEvents.BOOKING_REQUESTED]: BOOKING_REQUESTED,
|
||||
[WorkflowTriggerEvents.BOOKING_PAYMENT_INITIATED]: BOOKING_PAYMENT_INITIATED,
|
||||
[WorkflowTriggerEvents.BOOKING_PAID]: BOOKING_PAID,
|
||||
[WorkflowTriggerEvents.BOOKING_NO_SHOW_UPDATED]: BOOKING_NO_SHOW_UPDATED,
|
||||
} as const;
|
||||
|
||||
export const HOUR = "hour";
|
||||
@@ -108,6 +129,41 @@ export class OnCancelTriggerDto {
|
||||
type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
|
||||
}
|
||||
|
||||
export class OnRejectedTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
type: typeof BOOKING_REJECTED = BOOKING_REJECTED;
|
||||
}
|
||||
|
||||
export class OnRequestedTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
type: typeof BOOKING_REQUESTED = BOOKING_REQUESTED;
|
||||
}
|
||||
|
||||
export class OnPaymentInitiatedTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
type: typeof BOOKING_PAYMENT_INITIATED = BOOKING_PAYMENT_INITIATED;
|
||||
}
|
||||
|
||||
export class OnPaidTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
type: typeof BOOKING_PAID = BOOKING_PAID;
|
||||
}
|
||||
|
||||
export class OnNoShowUpdateTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
type: typeof BOOKING_NO_SHOW_UPDATED = BOOKING_NO_SHOW_UPDATED;
|
||||
}
|
||||
|
||||
export class TriggerOffsetDTO {
|
||||
@ApiProperty({
|
||||
description: "Offset before/after the trigger time; required for BEFORE_EVENT and AFTER_EVENT only",
|
||||
|
||||
@@ -2,11 +2,12 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { TimeUnit, Workflow, WorkflowStep, WorkflowTriggerEvents } from "@prisma/client";
|
||||
|
||||
import { TimeUnit, WorkflowTriggerEvents } from "@calcom/platform-libraries";
|
||||
import { TUpdateInputSchema } from "@calcom/platform-libraries/workflows";
|
||||
import { updateWorkflow } from "@calcom/platform-libraries/workflows";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
import type { Workflow, WorkflowStep } from "@calcom/prisma/client";
|
||||
|
||||
export type WorkflowType = Workflow & { activeOn: { eventTypeId: number }[]; steps: WorkflowStep[] };
|
||||
|
||||
|
||||
@@ -9858,6 +9858,7 @@
|
||||
"get": {
|
||||
"operationId": "EventTypesController_2024_06_14_getEventTypes",
|
||||
"summary": "Get all event types",
|
||||
"description": "Hidden event types are returned only if authentication is provided and it belongs to the event type owner.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
@@ -9913,6 +9914,33 @@
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "x-cal-secret-key",
|
||||
"in": "header",
|
||||
"description": "For platform customers - OAuth client secret key",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "x-cal-client-id",
|
||||
"in": "header",
|
||||
"description": "For platform customers - OAuth client ID",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -22554,7 +22582,12 @@
|
||||
"afterEvent",
|
||||
"rescheduleEvent",
|
||||
"afterHostsCalVideoNoShow",
|
||||
"afterGuestsCalVideoNoShow"
|
||||
"afterGuestsCalVideoNoShow",
|
||||
"bookingRejected",
|
||||
"bookingRequested",
|
||||
"bookingPaymentInitiated",
|
||||
"bookingPaid",
|
||||
"bookingNoShowUpdated"
|
||||
]
|
||||
},
|
||||
"offset": {
|
||||
@@ -22923,6 +22956,71 @@
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"OnNoShowUpdateTriggerDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "bookingNoShowUpdated",
|
||||
"description": "Trigger type for the workflow"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"OnRejectedTriggerDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "bookingRejected",
|
||||
"description": "Trigger type for the workflow"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"OnRequestedTriggerDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "bookingRequested",
|
||||
"description": "Trigger type for the workflow"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"OnPaymentInitiatedTriggerDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "bookingPaymentInitiated",
|
||||
"description": "Trigger type for the workflow"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"OnPaidTriggerDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "bookingPaid",
|
||||
"description": "Trigger type for the workflow"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"OnAfterCalVideoGuestsNoShowTriggerDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -23568,7 +23666,12 @@
|
||||
"afterEvent",
|
||||
"rescheduleEvent",
|
||||
"afterHostsCalVideoNoShow",
|
||||
"afterGuestsCalVideoNoShow"
|
||||
"afterGuestsCalVideoNoShow",
|
||||
"bookingRejected",
|
||||
"bookingRequested",
|
||||
"bookingPaymentInitiated",
|
||||
"bookingPaid",
|
||||
"bookingNoShowUpdated"
|
||||
]
|
||||
],
|
||||
"description": "Trigger type for the workflow"
|
||||
@@ -23642,6 +23745,21 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OnAfterCalVideoHostsNoShowTriggerDto"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OnRejectedTriggerDto"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OnRequestedTriggerDto"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OnPaidTriggerDto"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OnPaymentInitiatedTriggerDto"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OnNoShowUpdateTriggerDto"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -24298,6 +24416,21 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OnAfterCalVideoHostsNoShowTriggerDto"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OnRejectedTriggerDto"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OnRequestedTriggerDto"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OnPaidTriggerDto"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OnPaymentInitiatedTriggerDto"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OnNoShowUpdateTriggerDto"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TestingModule } from "@nestjs/testing";
|
||||
import { Membership, MembershipRole, Prisma, Team, User } from "@prisma/client";
|
||||
import { Membership, Prisma, Team, User } from "@prisma/client";
|
||||
|
||||
import { MembershipRole } from "@calcom/platform-libraries";
|
||||
|
||||
export class MembershipRepositoryFixture {
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user