Compare commits
88
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78f3d76ae2 | ||
|
|
6f9e04c6d9 | ||
|
|
fdb0e5d004 | ||
|
|
268a142370 | ||
|
|
29a3892669 | ||
|
|
0223027958 | ||
|
|
d7df154059 | ||
|
|
51d78e6f9d | ||
|
|
d5894d9fbc | ||
|
|
8b0539028e | ||
|
|
c7600d8ead | ||
|
|
6f5b8ead75 | ||
|
|
fc159fb9ea | ||
|
|
b3297f2843 | ||
|
|
1dade81299 | ||
|
|
b2839830c8 | ||
|
|
fa884e8516 | ||
|
|
3418a25a85 | ||
|
|
89cc175d73 | ||
|
|
c7c8564b63 | ||
|
|
b620581d7a | ||
|
|
c0530cc91c | ||
|
|
f738b3bc0a | ||
|
|
0853b4ca35 | ||
|
|
8be05b1dc5 | ||
|
|
71515ad17c | ||
|
|
66234ffba9 | ||
|
|
66c5f810ea | ||
|
|
81d9d84b4a | ||
|
|
6ddc666827 | ||
|
|
01346abb85 | ||
|
|
3d9650c841 | ||
|
|
f8ede75dd8 | ||
|
|
435587f043 | ||
|
|
e5abe93940 | ||
|
|
c3a5158a53 | ||
|
|
a9e14b9816 | ||
|
|
e207f40611 | ||
|
|
4ca2076c72 | ||
|
|
959b803d84 | ||
|
|
7f6136004c | ||
|
|
bb79368b2d | ||
|
|
70825dd138 | ||
|
|
05c399db9c | ||
|
|
57776b829b | ||
|
|
7fd28c39f3 | ||
|
|
ff17aa79fc | ||
|
|
578be32936 | ||
|
|
b183239ff5 | ||
|
|
013e614397 | ||
|
|
6843ced3db | ||
|
|
7318015a6e | ||
|
|
fc4d0a6aa3 | ||
|
|
8e0cdf3671 | ||
|
|
fcd9480f9f | ||
|
|
09ddbe886f | ||
|
|
4ac8a51bc2 | ||
|
|
82951cd04e | ||
|
|
ae87fdcc9f | ||
|
|
35e29c4bee | ||
|
|
387a1704f3 | ||
|
|
3fb8c5bbc5 | ||
|
|
626585f5d0 | ||
|
|
4d3727db12 | ||
|
|
f6641d5173 | ||
|
|
f9b21206a2 | ||
|
|
96821c0026 | ||
|
|
3d5e4754af | ||
|
|
613e9f0788 | ||
|
|
1261247e94 | ||
|
|
2ad843ffbb | ||
|
|
6923b97cd2 | ||
|
|
e639237088 | ||
|
|
5bc1fbb8c2 | ||
|
|
e310931417 | ||
|
|
17944311c3 | ||
|
|
d82f87aa2a | ||
|
|
6aa21aceb8 | ||
|
|
870ad57a7e | ||
|
|
fddee71829 | ||
|
|
6f15a5ab0f | ||
|
|
1fd64c5d7b | ||
|
|
f8614f11ff | ||
|
|
5e5e77236a | ||
|
|
4cee53b07c | ||
|
|
c289fac008 | ||
|
|
68f9e86cdd | ||
|
|
5da754db65 |
@@ -111,7 +111,7 @@ The event types page UI components are located in `apps/web/modules/event-types/
|
||||
|
||||
Changes to shared UI patterns (like tab layouts and button alignments) need to be checked across multiple views to maintain consistency:
|
||||
- Event types page layout: `apps/web/modules/event-types/views/event-types-listing-view.tsx`
|
||||
- Bookings page layout: `apps/web/modules/bookings/views/bookings-listing-view.tsx`
|
||||
- Bookings page layout: `apps/web/modules/bookings/views/bookings-view.tsx`
|
||||
- Common elements like tabs, search bars, and filter buttons should maintain consistent alignment across views
|
||||
|
||||
## When working on workflow triggers or similar enum-based features in the Cal.com codebase
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@calcom/atoms": minor
|
||||
---
|
||||
|
||||
Fix Vite “process is undefined” error by defining process.env in atoms build.
|
||||
@@ -486,3 +486,6 @@ CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY=
|
||||
SALESFORCE_GRAPHQL_DELAY_MS=500
|
||||
SALESFORCE_GRAPHQL_MAX_DELAY_MS=2000
|
||||
SALESFORCE_GRAPHQL_MAX_RETRIES=3
|
||||
|
||||
# Force Node.js to use UTC as process timezone
|
||||
TZ=UTC
|
||||
|
||||
@@ -26,7 +26,7 @@ runs:
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}
|
||||
- run: echo ${{ env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} && yarn db-seed
|
||||
- run: USE_POOL=true yarn db-seed
|
||||
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
- name: Postgres Dump Backup
|
||||
|
||||
@@ -56,8 +56,8 @@ jobs:
|
||||
if: steps.cache-api-v2-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn workspace @calcom/api-v2 run generate-schemas
|
||||
rm -rf apps/api/v2/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/api-v2 run generate-schemas &&
|
||||
rm -rf apps/api/v2/node_modules &&
|
||||
yarn install &&
|
||||
yarn workspace @calcom/api-v2 run build
|
||||
shell: bash
|
||||
|
||||
@@ -20,8 +20,8 @@ jobs:
|
||||
days-before-close: -1
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: -1
|
||||
days-before-pr-stale: 14
|
||||
days-before-pr-close: -1
|
||||
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
||||
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
|
||||
days-before-pr-stale: 7
|
||||
days-before-pr-close: 14
|
||||
stale-pr-message: "This PR has been marked as stale due to inactivity. If you're still working on it or need any help, please let us know or update the PR to keep it active."
|
||||
close-pr-message: "This PR has been closed due to inactivity. Please feel free to reopen it if you'd like to continue the work."
|
||||
operations-per-run: 100
|
||||
|
||||
@@ -16,7 +16,6 @@ env:
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
|
||||
@@ -10,7 +10,6 @@ env:
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
@@ -36,7 +35,6 @@ env:
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Web App
|
||||
|
||||
@@ -67,6 +67,9 @@ tsconfig.tsbuildinfo
|
||||
|
||||
# Prisma generated files
|
||||
packages/prisma/client/*
|
||||
!packages/prisma/client/index.ts
|
||||
apps/api/v2/generated/prisma/*
|
||||
packages/prisma/generated/prisma/*
|
||||
packages/prisma/zod/**/*.ts
|
||||
packages/prisma/enums
|
||||
|
||||
|
||||
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
@@ -234,9 +234,10 @@ for Logger level to be set at info, for example.
|
||||
|
||||
</details>
|
||||
|
||||
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
|
||||
If you don't want to create a local DB. Then you can also consider using services like railway.app, Northflank or render.
|
||||
|
||||
- [Setup postgres DB with railway.app](https://docs.railway.app/guides/postgresql)
|
||||
- [Setup postgres DB with Northflank](https://northflank.com/guides/deploy-postgres-database-on-northflank)
|
||||
- [Setup postgres DB with render](https://render.com/docs/databases)
|
||||
|
||||
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
|
||||
@@ -390,6 +391,12 @@ Cal.com, Inc. does not provide official support for Docker, but we will accept f
|
||||
|
||||
You can deploy Cal.com on [Railway](https://railway.app) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal.com on their platform.
|
||||
|
||||
### Northflank
|
||||
|
||||
[](https://northflank.com/stacks/deploy-calcom)
|
||||
|
||||
You can deploy Cal.com on [Northflank](https://northflank.com) using the button above. The team at Northflank also have a [detailed blog post](https://northflank.com/guides/deploy-calcom-with-northflank) on deploying Cal.com on their platform.
|
||||
|
||||
### Vercel
|
||||
|
||||
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { get } from "@vercel/edge-config";
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
const safeGet = async <T = unknown>(key: string): Promise<T | undefined> => {
|
||||
try {
|
||||
return get<T>(key);
|
||||
} catch (error) {
|
||||
// Don't crash if EDGE_CONFIG env var is missing
|
||||
}
|
||||
};
|
||||
|
||||
export const config = { matcher: "/:path*" };
|
||||
|
||||
export const checkIsInMaintenanceMode: NextMiddleware = async (req, res, next) => {
|
||||
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
|
||||
if (isInMaintenanceMode) {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: "API is currently under maintenance. Please try again at a later time." });
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
@@ -3,7 +3,6 @@ import { label } from "next-api-middleware";
|
||||
import { addRequestId } from "./addRequestid";
|
||||
import { captureErrors } from "./captureErrors";
|
||||
import { captureUserId } from "./captureUserId";
|
||||
import { checkIsInMaintenanceMode } from "./checkIsInMaintenanceMode";
|
||||
import { extendRequest } from "./extendRequest";
|
||||
import {
|
||||
HTTP_DELETE,
|
||||
@@ -26,7 +25,6 @@ const middleware = {
|
||||
HTTP_POST,
|
||||
HTTP_DELETE,
|
||||
addRequestId,
|
||||
checkIsInMaintenanceMode,
|
||||
verifyApiKey,
|
||||
rateLimitApiKey,
|
||||
extendRequest,
|
||||
@@ -38,16 +36,14 @@ const middleware = {
|
||||
|
||||
type Middleware = keyof typeof middleware;
|
||||
|
||||
const middlewareOrder = [
|
||||
// The order here, determines the order of execution
|
||||
"checkIsInMaintenanceMode",
|
||||
const middlewareOrder: Middleware[] = [
|
||||
"extendRequest",
|
||||
"captureErrors",
|
||||
"verifyApiKey",
|
||||
"rateLimitApiKey",
|
||||
"addRequestId",
|
||||
"captureUserId",
|
||||
] as Middleware[]; // <-- Provide a list of middleware to call automatically
|
||||
];
|
||||
|
||||
const withMiddleware = label(middleware, middlewareOrder);
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const getAllAdminMemberships = async (userId: number) => {
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] },
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
@@ -56,16 +56,19 @@ export const getAccessibleUsers = async ({
|
||||
memberUserIds,
|
||||
adminUserId,
|
||||
}: AccessibleUsersType): Promise<number[]> => {
|
||||
const orConditions = [];
|
||||
if (memberUserIds.length > 0) {
|
||||
orConditions.push({ userId: { in: memberUserIds } });
|
||||
}
|
||||
orConditions.push({ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } });
|
||||
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
team: {
|
||||
isOrganization: true,
|
||||
},
|
||||
accepted: true,
|
||||
OR: [
|
||||
{ userId: { in: memberUserIds } },
|
||||
{ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } },
|
||||
],
|
||||
OR: orConditions,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
|
||||
@@ -12,7 +12,7 @@ async function authMiddleware(req: NextApiRequest) {
|
||||
if (isSystemWideAdmin) return;
|
||||
// Find all references where the user has bookings
|
||||
const bookingReference = await prisma.bookingReference.findFirst({
|
||||
where: { id, booking: { userId } },
|
||||
where: { id, booking: { userId }, deleted: null },
|
||||
});
|
||||
if (!bookingReference) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
await prisma.bookingReference.delete({ where: { id } });
|
||||
await prisma.bookingReference.update({ where: { id }, data: { deleted: true } });
|
||||
return { message: `BookingReference with id: ${id} deleted` };
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id } });
|
||||
const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id, deleted: null } });
|
||||
return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) };
|
||||
}
|
||||
|
||||
|
||||
@@ -29,13 +29,13 @@ import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-refe
|
||||
* 404:
|
||||
* description: No booking references were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
export async function handler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.BookingReferenceFindManyArgs = isSystemWideAdmin
|
||||
? {}
|
||||
: { where: { booking: { userId } } };
|
||||
? { where: { deleted: null } }
|
||||
: { where: { booking: { userId }, deleted: null } };
|
||||
const data = await prisma.bookingReference.findMany(args);
|
||||
return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
export default defaultResponder(handler);
|
||||
|
||||
@@ -3,8 +3,8 @@ import utc from "dayjs/plugin/utc";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
||||
import { getAvailableSlotsService } from "@calcom/features/di/containers/AvailableSlots";
|
||||
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
@@ -48,7 +48,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
: availableSlots.slots;
|
||||
|
||||
return { slots: slotsInProvidedTimeZone };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
} catch (cause) {
|
||||
if (cause instanceof TRPCError) {
|
||||
const statusCode = getHTTPStatusCodeFromError(cause);
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Booking, Credential, EventType, User } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import { patchHandler } from "../../pages/api/booking-references/[id]/_patch";
|
||||
import { handler } from "../../pages/api/booking-references/_get";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
describe("GET /api/booking-references - Soft-delete filtering", () => {
|
||||
let testUser: User;
|
||||
let testCredential: Credential;
|
||||
let testEventType: EventType;
|
||||
let testBooking: Booking;
|
||||
const createdBookingReferenceIds: number[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
testUser = await prisma.user.create({
|
||||
data: {
|
||||
email: "api-bookingreference-test@example.com",
|
||||
username: "api-bookingreference-test",
|
||||
},
|
||||
});
|
||||
|
||||
testCredential = await prisma.credential.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
key: {},
|
||||
userId: testUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
testEventType = await prisma.eventType.create({
|
||||
data: {
|
||||
title: "Test Event Type",
|
||||
slug: "api-test-event-type",
|
||||
length: 30,
|
||||
userId: testUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
testBooking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: "api-test-booking-uid",
|
||||
title: "API Test Booking",
|
||||
startTime: new Date(Date.now() + 86400000),
|
||||
endTime: new Date(Date.now() + 90000000),
|
||||
userId: testUser.id,
|
||||
eventTypeId: testEventType.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (createdBookingReferenceIds.length > 0) {
|
||||
await prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: createdBookingReferenceIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.booking.delete({
|
||||
where: {
|
||||
id: testBooking.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.eventType.delete({
|
||||
where: {
|
||||
id: testEventType.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.credential.delete({
|
||||
where: {
|
||||
id: testCredential.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.delete({
|
||||
where: {
|
||||
id: testUser.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should only return active booking references, excluding soft-deleted ones", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
uid: `api-active-ref-${timestamp}`,
|
||||
meetingId: `api-active-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef.id);
|
||||
|
||||
const deletedRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
uid: `api-deleted-ref-${timestamp}`,
|
||||
meetingId: `api-deleted-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
deleted: true,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(deletedRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
|
||||
const responseData = await handler(req);
|
||||
|
||||
expect(responseData.booking_references).toBeDefined();
|
||||
expect(Array.isArray(responseData.booking_references)).toBe(true);
|
||||
|
||||
const returnedActiveRef = responseData.booking_references.find(
|
||||
(ref: { id: number }) => ref.id === activeRef.id
|
||||
);
|
||||
const returnedDeletedRef = responseData.booking_references.find(
|
||||
(ref: { id: number }) => ref.id === deletedRef.id
|
||||
);
|
||||
|
||||
expect(returnedActiveRef).toBeDefined();
|
||||
expect(returnedDeletedRef).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should filter booking references by user when not system admin", async () => {
|
||||
const timestamp = Date.now();
|
||||
const otherUser = await prisma.user.create({
|
||||
data: {
|
||||
email: `other-user-bookingreference-${timestamp}@example.com`,
|
||||
username: `other-user-bookingreference-${timestamp}`,
|
||||
},
|
||||
});
|
||||
|
||||
const otherBooking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: `other-user-booking-uid-${timestamp}`,
|
||||
title: "Other User Booking",
|
||||
startTime: new Date(Date.now() + 86400000),
|
||||
endTime: new Date(Date.now() + 90000000),
|
||||
userId: otherUser.id,
|
||||
eventTypeId: testEventType.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
|
||||
const otherUserRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "zoom_video",
|
||||
uid: `other-user-ref-${timestamp}`,
|
||||
meetingId: `other-user-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: otherBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(otherUserRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
|
||||
const responseData = await handler(req);
|
||||
|
||||
const userRefs = responseData.booking_references.filter(
|
||||
(ref: { id: number }) => ref.id === otherUserRef.id
|
||||
);
|
||||
|
||||
expect(userRefs).toHaveLength(0);
|
||||
|
||||
await prisma.booking.delete({ where: { id: otherBooking.id } });
|
||||
await prisma.user.delete({ where: { id: otherUser.id } });
|
||||
});
|
||||
|
||||
it("should return only active booking references for user, not soft-deleted ones for system admin", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef1 = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "daily_video",
|
||||
uid: `admin-active-ref-1-${timestamp}`,
|
||||
meetingId: `admin-active-meeting-1-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef1.id);
|
||||
|
||||
const activeRef2 = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "daily_video",
|
||||
uid: `admin-active-ref-2-${timestamp}`,
|
||||
meetingId: `admin-active-meeting-2-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef2.id);
|
||||
|
||||
const deletedRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "daily_video",
|
||||
uid: `admin-deleted-ref-${timestamp}`,
|
||||
meetingId: `admin-deleted-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
deleted: true,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(deletedRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
|
||||
const responseData = await handler(req);
|
||||
|
||||
const activeRefs = responseData.booking_references.filter(
|
||||
(ref: { id: number }) => ref.id === activeRef1.id || ref.id === activeRef2.id
|
||||
);
|
||||
const returnedDeletedRef = responseData.booking_references.find(
|
||||
(ref: { id: number }) => ref.id === deletedRef.id
|
||||
);
|
||||
|
||||
expect(activeRefs.length).toBe(2);
|
||||
expect(returnedDeletedRef).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should verify soft-deleted references are never returned through the API", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "office365_calendar",
|
||||
uid: `never-return-active-${timestamp}`,
|
||||
meetingId: `never-return-active-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef.id);
|
||||
|
||||
await prisma.bookingReference.update({
|
||||
where: { id: activeRef.id },
|
||||
data: { deleted: true },
|
||||
});
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
|
||||
const responseData = await handler(req);
|
||||
|
||||
const softDeletedRef = responseData.booking_references.find(
|
||||
(ref: { id: number }) => ref.id === activeRef.id
|
||||
);
|
||||
|
||||
expect(softDeletedRef).toBeUndefined();
|
||||
|
||||
const refInDb = await prisma.bookingReference.findUnique({
|
||||
where: { id: activeRef.id },
|
||||
});
|
||||
expect(refInDb).toBeDefined();
|
||||
expect(refInDb?.deleted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/booking-references/[id] - Existing functionality", () => {
|
||||
let testUser: User;
|
||||
let testCredential: Credential;
|
||||
let testEventType: EventType;
|
||||
let testBooking: Booking;
|
||||
const createdBookingReferenceIds: number[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
testUser = await prisma.user.create({
|
||||
data: {
|
||||
email: "api-bookingreference-patch-test@example.com",
|
||||
username: "api-bookingreference-patch-test",
|
||||
},
|
||||
});
|
||||
|
||||
testCredential = await prisma.credential.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
key: {},
|
||||
userId: testUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
testEventType = await prisma.eventType.create({
|
||||
data: {
|
||||
title: "Test Event Type",
|
||||
slug: "api-patch-test-event-type",
|
||||
length: 30,
|
||||
userId: testUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
testBooking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: "api-patch-test-booking-uid",
|
||||
title: "API Patch Test Booking",
|
||||
startTime: new Date(Date.now() + 86400000),
|
||||
endTime: new Date(Date.now() + 90000000),
|
||||
userId: testUser.id,
|
||||
eventTypeId: testEventType.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (createdBookingReferenceIds.length > 0) {
|
||||
await prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: createdBookingReferenceIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.booking.delete({
|
||||
where: {
|
||||
id: testBooking.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.eventType.delete({
|
||||
where: {
|
||||
id: testEventType.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.credential.delete({
|
||||
where: {
|
||||
id: testCredential.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.delete({
|
||||
where: {
|
||||
id: testUser.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should successfully update an active booking reference", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
uid: `patch-active-ref-${timestamp}`,
|
||||
meetingId: `patch-active-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "PATCH",
|
||||
query: { id: activeRef.id.toString() },
|
||||
body: {
|
||||
meetingPassword: "new-password-123",
|
||||
},
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
req.isSystemWideAdmin = false;
|
||||
|
||||
const responseData = await patchHandler(req);
|
||||
|
||||
expect(responseData.booking_reference).toBeDefined();
|
||||
expect(responseData.booking_reference.id).toBe(activeRef.id);
|
||||
expect(responseData.booking_reference.meetingPassword).toBe("new-password-123");
|
||||
|
||||
const updatedRef = await prisma.bookingReference.findUnique({
|
||||
where: { id: activeRef.id },
|
||||
});
|
||||
expect(updatedRef?.meetingPassword).toBe("new-password-123");
|
||||
});
|
||||
|
||||
it("should allow updating a soft-deleted booking reference (existing functionality)", async () => {
|
||||
const timestamp = Date.now();
|
||||
const deletedRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
uid: `patch-deleted-ref-${timestamp}`,
|
||||
meetingId: `patch-deleted-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
deleted: true,
|
||||
meetingPassword: "original-password",
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(deletedRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "PATCH",
|
||||
query: { id: deletedRef.id.toString() },
|
||||
body: {
|
||||
meetingPassword: "updated-password",
|
||||
},
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
req.isSystemWideAdmin = false;
|
||||
|
||||
const responseData = await patchHandler(req);
|
||||
|
||||
expect(responseData.booking_reference).toBeDefined();
|
||||
expect(responseData.booking_reference.id).toBe(deletedRef.id);
|
||||
expect(responseData.booking_reference.meetingPassword).toBe("updated-password");
|
||||
|
||||
const updatedRef = await prisma.bookingReference.findUnique({
|
||||
where: { id: deletedRef.id },
|
||||
});
|
||||
expect(updatedRef?.meetingPassword).toBe("updated-password");
|
||||
expect(updatedRef?.deleted).toBe(true);
|
||||
});
|
||||
|
||||
it("should verify that booking references can be updated regardless of deleted status", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "zoom_video",
|
||||
uid: `patch-verify-active-${timestamp}`,
|
||||
meetingId: `patch-verify-active-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
meetingPassword: "original-password",
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef.id);
|
||||
|
||||
await prisma.bookingReference.update({
|
||||
where: { id: activeRef.id },
|
||||
data: { deleted: true },
|
||||
});
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "PATCH",
|
||||
query: { id: activeRef.id.toString() },
|
||||
body: {
|
||||
meetingPassword: "updated-password",
|
||||
},
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
req.isSystemWideAdmin = false;
|
||||
|
||||
const responseData = await patchHandler(req);
|
||||
|
||||
expect(responseData.booking_reference).toBeDefined();
|
||||
expect(responseData.booking_reference.meetingPassword).toBe("updated-password");
|
||||
|
||||
const refInDb = await prisma.bookingReference.findUnique({
|
||||
where: { id: activeRef.id },
|
||||
});
|
||||
expect(refInDb?.meetingPassword).toBe("updated-password");
|
||||
expect(refInDb?.deleted).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -11,6 +11,30 @@ type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
describe("PATCH /api/bookings", () => {
|
||||
beforeAll(async () => {
|
||||
const acmeOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "acme",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (acmeOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: acmeOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
create: {
|
||||
organizationId: acmeOrg.id,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
it("Returns 403 when user has no permission to the booking", async () => {
|
||||
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
|
||||
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||
|
||||
@@ -5,11 +5,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
import {
|
||||
getRecordingsOfCalVideoByRoomName,
|
||||
getDownloadLinkOfCalVideoByRecordingId,
|
||||
} from "@calcom/features/conferencing/lib/videoClient";
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/features/conferencing/lib/videoClient";
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, beforeAll } from "vitest";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { handler } from "../../../pages/api/bookings/_get";
|
||||
|
||||
@@ -17,6 +17,30 @@ const DefaultPagination = {
|
||||
};
|
||||
|
||||
describe("GET /api/bookings", async () => {
|
||||
beforeAll(async () => {
|
||||
const acmeOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "acme",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (acmeOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: acmeOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
create: {
|
||||
organizationId: acmeOrg.id,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||
const proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
|
||||
|
||||
@@ -193,6 +217,22 @@ describe("GET /api/bookings", async () => {
|
||||
describe("Expand feature to add relational data in return payload", () => {
|
||||
it("Returns only team data when expand=team is set", async () => {
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
||||
|
||||
// Find a team booking and a non-team booking from seed data
|
||||
const team1 = await prisma.team.findFirst({ where: { slug: "team1" } });
|
||||
const teamEventType = await prisma.eventType.findFirst({
|
||||
where: { teamId: team1?.id },
|
||||
include: { bookings: true },
|
||||
});
|
||||
const teamBooking = teamEventType?.bookings[0];
|
||||
|
||||
const nonTeamBooking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
eventType: { teamId: null },
|
||||
userId: proUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
query: {
|
||||
@@ -205,14 +245,23 @@ describe("GET /api/bookings", async () => {
|
||||
req.isOrganizationOwnerOrAdmin = true;
|
||||
|
||||
const responseData = await handler(req);
|
||||
console.log("bookings=>", responseData.bookings);
|
||||
responseData.bookings.forEach((booking) => {
|
||||
if (booking.id === 31) expect(booking.eventType?.team?.slug).toBe("team1");
|
||||
if (booking.id === 19) {
|
||||
// The team field can be either null or undefined due to nullish() in the schema
|
||||
expect(booking.eventType?.team === null || booking.eventType?.team === undefined).toBe(true);
|
||||
|
||||
// Verify team booking has team data
|
||||
if (teamBooking) {
|
||||
const returnedTeamBooking = responseData.bookings.find((b) => b.id === teamBooking.id);
|
||||
expect(returnedTeamBooking?.eventType?.team?.slug).toBe("team1");
|
||||
}
|
||||
|
||||
// Verify non-team booking has null/undefined team
|
||||
if (nonTeamBooking) {
|
||||
const returnedNonTeamBooking = responseData.bookings.find((b) => b.id === nonTeamBooking.id);
|
||||
if (returnedNonTeamBooking) {
|
||||
expect(
|
||||
returnedNonTeamBooking.eventType?.team === null ||
|
||||
returnedNonTeamBooking.eventType?.team === undefined
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -318,11 +367,6 @@ describe("GET /api/bookings", async () => {
|
||||
|
||||
const testUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||
|
||||
const testUserBooking = await prisma.booking.findFirstOrThrow({
|
||||
where: { userId: testUser.id },
|
||||
include: { attendees: true },
|
||||
});
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
query: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -12,6 +12,53 @@ type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
describe("isAdmin guard", () => {
|
||||
beforeAll(async () => {
|
||||
const acmeOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "acme",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (acmeOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: acmeOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
create: {
|
||||
organizationId: acmeOrg.id,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const dunderOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "dunder-mifflin",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (dunderOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: dunderOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: false,
|
||||
},
|
||||
create: {
|
||||
organizationId: dunderOrg.id,
|
||||
orgAutoAcceptEmail: "dunder-mifflin.com",
|
||||
isAdminAPIEnabled: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
it("Returns false when user does not exist in the system", async () => {
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -8,6 +8,30 @@ import {
|
||||
} from "../../../lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
describe("retrieveScopedAccessibleUsers tests", () => {
|
||||
beforeAll(async () => {
|
||||
const acmeOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "acme",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (acmeOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: acmeOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
create: {
|
||||
organizationId: acmeOrg.id,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
describe("getAccessibleUsers", () => {
|
||||
it("Does not return members when only admin user ID is supplied", async () => {
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
||||
|
||||
@@ -42,4 +42,5 @@ lerna-debug.log*
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.appStore.example
|
||||
/client/*
|
||||
/client/*
|
||||
/generated/prisma/*
|
||||
|
||||
@@ -22,6 +22,10 @@ const loadConfig = (): AppConfig => {
|
||||
db: {
|
||||
readUrl: getEnv("DATABASE_READ_URL"),
|
||||
writeUrl: getEnv("DATABASE_WRITE_URL"),
|
||||
readPoolMax: getEnv("DATABASE_READ_POOL_MAX", 9),
|
||||
writePoolMax: getEnv("DATABASE_WRITE_POOL_MAX", 7),
|
||||
workerReadPoolMax: getEnv("DATABASE_READ_WORKER_POOL_MAX", 4),
|
||||
workerWritePoolMax: getEnv("DATABASE_WRITE_WORKER_POOL_MAX", 6),
|
||||
redisUrl: getEnv("REDIS_URL"),
|
||||
},
|
||||
next: {
|
||||
|
||||
@@ -13,6 +13,10 @@ export type AppConfig = {
|
||||
db: {
|
||||
readUrl: string;
|
||||
writeUrl: string;
|
||||
readPoolMax: number;
|
||||
writePoolMax: number;
|
||||
workerReadPoolMax: number;
|
||||
workerWritePoolMax: number;
|
||||
redisUrl: string;
|
||||
};
|
||||
next: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
|
||||
import { CreateRecurringBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-recurring-booking.input";
|
||||
import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output";
|
||||
import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
@@ -21,6 +22,7 @@ import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { BookingResponse } from "@calcom/platform-libraries";
|
||||
import { type RegularBookingCreateResult } from "@calcom/platform-libraries/bookings";
|
||||
import type { ApiSuccessResponse, ApiErrorResponse } from "@calcom/platform-types";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
@@ -40,6 +42,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
||||
let user: User;
|
||||
|
||||
let eventTypeId: number;
|
||||
let recEventTypeId: number;
|
||||
|
||||
let createdBooking: RegularBookingCreateResult;
|
||||
|
||||
@@ -81,6 +84,18 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
const recEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `rec-bookings-2024-04-15-event-type-${randomString()}-${describe.name}`,
|
||||
slug: `rec-bookings-2024-04-15-event-type-${randomString()}-${describe.name}`,
|
||||
length: 60,
|
||||
recurringEvent: { freq: 2, count: 4, interval: 1 },
|
||||
},
|
||||
user.id
|
||||
);
|
||||
recEventTypeId = recEventType.id;
|
||||
|
||||
eventTypeId = event.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
@@ -404,6 +419,94 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should create recurring bookings", async () => {
|
||||
const bookingEventTypeId = recEventTypeId;
|
||||
const bookingTimeZone = "Europe/London";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
const bookingMetadata = {
|
||||
timeFormat: "12",
|
||||
meetingType: "organizer-phone",
|
||||
};
|
||||
const bookingResponses = {
|
||||
name: "tester",
|
||||
email: "tester@example.com",
|
||||
location: {
|
||||
value: "link",
|
||||
optionValue: "",
|
||||
},
|
||||
notes: "test",
|
||||
};
|
||||
|
||||
const body: CreateRecurringBookingInput_2024_04_15[] = [
|
||||
{
|
||||
start: "2040-06-21T09:30:00.000Z",
|
||||
end: "2040-06-21T10:30:00.000Z",
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: bookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
responses: bookingResponses,
|
||||
recurringEventId: "test-recurring-event-id",
|
||||
},
|
||||
{
|
||||
start: "2040-06-27T09:30:00.000Z",
|
||||
end: "2040-06-27T10:30:00.000Z",
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: bookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
responses: bookingResponses,
|
||||
recurringEventId: "test-recurring-event-id-2",
|
||||
},
|
||||
{
|
||||
start: "2040-06-04T09:30:00.000Z",
|
||||
end: "2040-06-04T10:30:00.000Z",
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: bookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
responses: bookingResponses,
|
||||
recurringEventId: "test-recurring-event-id-3",
|
||||
},
|
||||
{
|
||||
start: "2040-06-11T09:30:00.000Z",
|
||||
end: "2040-06-11T10:30:00.000Z",
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: bookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
responses: bookingResponses,
|
||||
recurringEventId: "test-recurring-event-id-4",
|
||||
},
|
||||
];
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings/recurring")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<BookingResponse[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
responseBody.data.forEach((booking) => {
|
||||
expect(booking.userPrimaryEmail).toBeDefined();
|
||||
expect(booking.userPrimaryEmail).toEqual(userEmail);
|
||||
expect(booking.id).toBeDefined();
|
||||
expect(booking.uid).toBeDefined();
|
||||
expect(booking.eventTypeId).toEqual(bookingEventTypeId);
|
||||
expect(booking.user.timeZone).toEqual(bookingTimeZone);
|
||||
expect(booking.metadata).toEqual(bookingMetadata);
|
||||
expect(booking.responses).toEqual(bookingResponses);
|
||||
expect(booking.creationSource).toEqual("API_V2");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
|
||||
@@ -318,11 +318,9 @@ export class BookingsController_2024_04_15 {
|
||||
recurringEvent.recurringEventId = recurringEventId;
|
||||
}
|
||||
}
|
||||
|
||||
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
|
||||
|
||||
const createdBookings: BookingResponse[] = await this.recurringBookingService.createBooking({
|
||||
bookingData: bookingRequest.body,
|
||||
bookingData: body.map((booking) => ({ ...booking, creationSource: CreationSource.API_V2 })),
|
||||
bookingMeta: {
|
||||
userId: bookingRequest.userId,
|
||||
hostname: bookingRequest.headers?.host || "",
|
||||
@@ -331,6 +329,7 @@ export class BookingsController_2024_04_15 {
|
||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||
noEmail: bookingRequest.body.noEmail,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export class CreateRecurringBookingInput_2024_04_15 extends CreateBookingInput_2
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Object] })
|
||||
allRecurringDates?: Record<string, string>[];
|
||||
allRecurringDates?: { start: string; end: string | undefined }[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { BookingGuestsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-guests.controller";
|
||||
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { BookingGuestsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-guests.service";
|
||||
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||
@@ -15,6 +17,7 @@ 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";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { InstantBookingModule } from "@/lib/modules/instant-booking.module";
|
||||
import { RecurringBookingModule } from "@/lib/modules/recurring-booking.module";
|
||||
@@ -71,9 +74,11 @@ import { Module } from "@nestjs/common";
|
||||
OAuthClientRepository,
|
||||
OAuthClientUsersService,
|
||||
BookingsService_2024_08_13,
|
||||
BookingGuestsService_2024_08_13,
|
||||
InputBookingsService_2024_08_13,
|
||||
OutputBookingsService_2024_08_13,
|
||||
OutputBookingReferencesService_2024_08_13,
|
||||
OutputEventTypesService_2024_06_14,
|
||||
BookingsRepository_2024_08_13,
|
||||
EventTypesRepository_2024_06_14,
|
||||
BookingSeatRepository,
|
||||
@@ -92,7 +97,7 @@ import { Module } from "@nestjs/common";
|
||||
BookingReferencesRepository_2024_08_13,
|
||||
CalVideoService,
|
||||
],
|
||||
controllers: [BookingsController_2024_08_13],
|
||||
controllers: [BookingsController_2024_08_13, BookingGuestsController_2024_08_13],
|
||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||
})
|
||||
export class BookingsModule_2024_08_13 {}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
||||
import { AddGuestsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-guests.output";
|
||||
import { BookingGuestsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-guests.service";
|
||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
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 { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { Controller, Post, Logger, Body, UseGuards, Param, HttpCode, HttpStatus } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags, ApiHeader } from "@nestjs/swagger";
|
||||
|
||||
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { AddGuestsInput_2024_08_13 } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/bookings/:bookingUid/guests",
|
||||
version: VERSION_2024_08_13_VALUE,
|
||||
})
|
||||
@UseGuards(PermissionsGuard)
|
||||
@DocsTags("Bookings / Guests")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to ${VERSION_2024_08_13}. This header is required as this endpoint does not exist in older API versions.`,
|
||||
example: VERSION_2024_08_13,
|
||||
required: true,
|
||||
})
|
||||
export class BookingGuestsController_2024_08_13 {
|
||||
private readonly logger = new Logger("BookingGuestsController_2024_08_13");
|
||||
|
||||
constructor(private readonly bookingGuestsService: BookingGuestsService_2024_08_13) {}
|
||||
|
||||
@Post("/")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Add guests to an existing booking",
|
||||
description: `Add one or more guests to an existing booking.
|
||||
|
||||
**Email Notifications:**
|
||||
When guests are added, the following notifications are sent (unless disabled by event type settings):
|
||||
|
||||
- **Organizer & Team Members:** Receive an "Add Guests" notification email informing them that new guests have been added to the booking.
|
||||
|
||||
- **New Guests:** Receive a "Scheduled Event" email with full booking details and calendar invite. If they have a phone number, they also receive an SMS notification.
|
||||
|
||||
- **Existing Guests:** Receive an "Add Guests" notification email informing them that additional guests have been added to the booking.
|
||||
|
||||
<Note>The cal-api-version header is required for this endpoint. Without it, the request will fail with a 404 error.</Note>
|
||||
`,
|
||||
})
|
||||
async addGuests(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: AddGuestsInput_2024_08_13,
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<AddGuestsOutput_2024_08_13> {
|
||||
const booking = await this.bookingGuestsService.addGuests(bookingUid, body, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,8 @@ export class BookingsController_2024_08_13 {
|
||||
If you are creating a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||
|
||||
For event types that have SMS reminders workflow, you need to pass the attendee's phone number in the request body via \`attendee.phoneNumber\` (e.g., "+19876543210" in international format). This is an optional field, but becomes required when SMS reminders are enabled for the event type. For the complete attendee object structure, see the [attendee object](https://cal.com/docs/api-reference/v2/bookings/create-a-booking#body-attendee) documentation.
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
@@ -320,6 +322,9 @@ export class BookingsController_2024_08_13 {
|
||||
summary: "Cancel a booking",
|
||||
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
|
||||
|
||||
\nCancelling normal bookings:
|
||||
If the booking is not seated and not recurring, simply pass :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and optionally cancellationReason in the request body \`{"cancellationReason": "Will travel"}\`.
|
||||
|
||||
\nCancelling seated bookings:
|
||||
It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.
|
||||
\n1. As an attendee - provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and seatUid in the request body \`{"seatUid": "123-123-123"}\` . This will remove this particular attendance from the booking.
|
||||
@@ -343,7 +348,7 @@ export class BookingsController_2024_08_13 {
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Accepts different types of cancel booking input: Cancel Booking (Option 1) or Cancel Seated Booking (Option 2)",
|
||||
"Accepts different types of cancel booking input: Cancel Booking (Option 1 which is for normal or recurring bookings) or Cancel Seated Booking (Option 2 which is for seated bookings)",
|
||||
})
|
||||
@ApiExtraModels(CancelBookingInput_2024_08_13, CancelSeatedBookingInput_2024_08_13)
|
||||
async cancelBooking(
|
||||
|
||||
@@ -0,0 +1,523 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { AddGuestsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-guests.output";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import {
|
||||
CAL_API_VERSION_HEADER,
|
||||
ERROR_STATUS,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_08_13,
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
AttendeeAddGuestsEmail,
|
||||
AttendeeScheduledEmail,
|
||||
OrganizerAddGuestsEmail,
|
||||
} from "@calcom/platform-libraries/emails";
|
||||
import type { BookingOutput_2024_08_13, CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import type { User, Team } from "@calcom/prisma/client";
|
||||
|
||||
const attendeeAddGuestsEmailSpy = jest
|
||||
.spyOn(AttendeeAddGuestsEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
const organizerAddGuestsEmailSpy = jest
|
||||
.spyOn(OrganizerAddGuestsEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
const attendeeScheduledEmailSpy = jest
|
||||
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
|
||||
type TestUser = {
|
||||
user: User;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
type TestSetup = {
|
||||
organizer: TestUser;
|
||||
unrelatedUser: TestUser;
|
||||
guest: TestUser;
|
||||
eventTypeId: number;
|
||||
bookingUid: string;
|
||||
};
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13 add guests", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||
|
||||
let testSetup: TestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `user-emails-2024-08-13-organization-${randomString()}`,
|
||||
});
|
||||
|
||||
await setupTestData();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
const oAuthClient = await createOAuthClient(organization.id, true);
|
||||
|
||||
const organizerUser = await userRepositoryFixture.create({
|
||||
email: `user-emails-2024-08-13-organizer-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const unrelatedUserData = await userRepositoryFixture.create({
|
||||
email: `user-emails-2024-08-13-unrelated-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const guestUser = await userRepositoryFixture.create({
|
||||
email: `user-emails-2024-08-13-guest-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const organizerTokens = await tokensRepositoryFixture.createTokens(organizerUser.id, oAuthClient.id);
|
||||
const unrelatedUserTokens = await tokensRepositoryFixture.createTokens(
|
||||
unrelatedUserData.id,
|
||||
oAuthClient.id
|
||||
);
|
||||
const guestTokens = await tokensRepositoryFixture.createTokens(guestUser.id, oAuthClient.id);
|
||||
|
||||
const schedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `user-add-guests-2024-08-13-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(organizerUser.id, schedule);
|
||||
|
||||
const eventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `user-add-guests-2024-08-13-event-type-${randomString()}`,
|
||||
slug: `user-add-guests-2024-08-13-event-type-${randomString()}`,
|
||||
length: 60,
|
||||
},
|
||||
organizerUser.id
|
||||
);
|
||||
|
||||
testSetup = {
|
||||
organizer: {
|
||||
user: organizerUser,
|
||||
accessToken: organizerTokens.accessToken,
|
||||
refreshToken: organizerTokens.refreshToken,
|
||||
},
|
||||
unrelatedUser: {
|
||||
user: unrelatedUserData,
|
||||
accessToken: unrelatedUserTokens.accessToken,
|
||||
refreshToken: unrelatedUserTokens.refreshToken,
|
||||
},
|
||||
guest: {
|
||||
user: guestUser,
|
||||
accessToken: guestTokens.accessToken,
|
||||
refreshToken: guestTokens.refreshToken,
|
||||
},
|
||||
eventTypeId: eventType.id,
|
||||
bookingUid: "",
|
||||
};
|
||||
}
|
||||
|
||||
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
areEmailsEnabled: emailsEnabled,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
attendeeAddGuestsEmailSpy.mockClear();
|
||||
organizerAddGuestsEmailSpy.mockClear();
|
||||
attendeeScheduledEmailSpy.mockClear();
|
||||
});
|
||||
|
||||
describe("POST /v2/bookings/:bookingUid/guests", () => {
|
||||
beforeAll(async () => {
|
||||
const createBookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: testSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
const createBookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(createBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const createBookingResponseBody: CreateBookingOutput_2024_08_13 = createBookingResponse.body;
|
||||
expect(createBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
if (!responseDataIsBooking(createBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
|
||||
testSetup.bookingUid = createBookingResponseBody.data.uid;
|
||||
});
|
||||
|
||||
describe("Authentication", () => {
|
||||
it("should return 401 when adding guests without authentication", async () => {
|
||||
const addGuestsBody = {
|
||||
guests: [{ email: "unauthenticated.guest@example.com" }],
|
||||
};
|
||||
|
||||
const addGuestsResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
|
||||
|
||||
expect(addGuestsResponse.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authorization - Organizer", () => {
|
||||
it("should allow booking organizer to add multiple guests", async () => {
|
||||
const addGuestsBody = {
|
||||
guests: [{ email: "organizer.guest1@example.com" }, { email: "organizer.guest2@example.com" }],
|
||||
};
|
||||
|
||||
const addGuestsResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const addGuestsResponseBody: AddGuestsOutput_2024_08_13 = addGuestsResponse.body;
|
||||
|
||||
verifyAddGuestsResponse(
|
||||
addGuestsResponseBody,
|
||||
["organizer.guest1@example.com", "organizer.guest2@example.com"],
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("should return 403 when unrelated user tries to add guests", async () => {
|
||||
const addGuestsBody = {
|
||||
guests: [
|
||||
{ email: "non-organizer.guest1@example.com" },
|
||||
{ email: "non-organizer.guest2@example.com" },
|
||||
],
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authorization - guest", () => {
|
||||
it("should allow organizer to add a user as guest, then guest can add more guests", async () => {
|
||||
// Step 1: Organizer adds guest user as a guest
|
||||
const addguestBody = {
|
||||
guests: [{ email: testSetup.guest.user.email }],
|
||||
};
|
||||
|
||||
const addguestResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addguestBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const addguestResponseBody: AddGuestsOutput_2024_08_13 = addguestResponse.body;
|
||||
|
||||
verifyAddGuestsResponse(addguestResponseBody, [testSetup.guest.user.email], true);
|
||||
|
||||
// Step 2: Now the guest can add their own guests
|
||||
const addGuestsBody = {
|
||||
guests: [{ email: "guest.guest1@example.com" }],
|
||||
};
|
||||
|
||||
const addGuestsResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.guest.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const addGuestsResponseBody: AddGuestsOutput_2024_08_13 = addGuestsResponse.body;
|
||||
|
||||
verifyAddGuestsResponse(addGuestsResponseBody, ["guest.guest1@example.com"], true);
|
||||
});
|
||||
|
||||
it("should return 403 when non-guest user tries to add guests", async () => {
|
||||
const addGuestsBody = {
|
||||
guests: [{ email: "non-guest.guest1@example.com" }, { email: "non-guest.guest2@example.com" }],
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Validation", () => {
|
||||
it("should return 400 when attempting to add duplicate guest email", async () => {
|
||||
const addGuestsBody = {
|
||||
guests: [{ email: "organizer.guest1@example.com" }],
|
||||
};
|
||||
|
||||
const addGuestsResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`);
|
||||
|
||||
expect(addGuestsResponse.status).toEqual(400);
|
||||
expect(addGuestsResponse.body.status).toEqual(ERROR_STATUS);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /v2/bookings/:bookingUid/guests - Emails Disabled", () => {
|
||||
let emailsDisabledSetup: TestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
const oAuthClientDisabled = await createOAuthClient(organization.id, false);
|
||||
|
||||
const organizerUser = await userRepositoryFixture.create({
|
||||
email: `user-no-emails-2024-08-13-organizer-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClientDisabled.id },
|
||||
},
|
||||
});
|
||||
|
||||
const organizerTokens = await tokensRepositoryFixture.createTokens(
|
||||
organizerUser.id,
|
||||
oAuthClientDisabled.id
|
||||
);
|
||||
|
||||
const schedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `user-no-emails-2024-08-13-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(organizerUser.id, schedule);
|
||||
|
||||
const eventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `user-no-emails-2024-08-13-event-type-${randomString()}`,
|
||||
slug: `user-no-emails-2024-08-13-event-type-${randomString()}`,
|
||||
length: 60,
|
||||
},
|
||||
organizerUser.id
|
||||
);
|
||||
|
||||
const createBookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
|
||||
eventTypeId: eventType.id,
|
||||
attendee: {
|
||||
name: "No Email User",
|
||||
email: "no.email.user@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/no-emails",
|
||||
bookingFieldsResponses: {
|
||||
customField: "noEmailValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "200",
|
||||
},
|
||||
};
|
||||
|
||||
const createBookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(createBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const createBookingResponseBody: CreateBookingOutput_2024_08_13 = createBookingResponse.body;
|
||||
|
||||
if (!responseDataIsBooking(createBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
|
||||
emailsDisabledSetup = {
|
||||
organizer: {
|
||||
user: organizerUser,
|
||||
accessToken: organizerTokens.accessToken,
|
||||
refreshToken: organizerTokens.refreshToken,
|
||||
},
|
||||
unrelatedUser: testSetup.unrelatedUser,
|
||||
guest: testSetup.guest,
|
||||
eventTypeId: eventType.id,
|
||||
bookingUid: createBookingResponseBody.data.uid,
|
||||
};
|
||||
});
|
||||
|
||||
it("should NOT send emails when adding guests (emails disabled)", async () => {
|
||||
attendeeAddGuestsEmailSpy.mockClear();
|
||||
organizerAddGuestsEmailSpy.mockClear();
|
||||
attendeeScheduledEmailSpy.mockClear();
|
||||
|
||||
const addGuestsBody = {
|
||||
guests: [{ email: "no-email.guest1@example.com" }, { email: "no-email.guest2@example.com" }],
|
||||
};
|
||||
|
||||
const addGuestsResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsDisabledSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${emailsDisabledSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const addGuestsResponseBody: AddGuestsOutput_2024_08_13 = addGuestsResponse.body;
|
||||
|
||||
verifyAddGuestsResponse(
|
||||
addGuestsResponseBody,
|
||||
["no-email.guest1@example.com", "no-email.guest2@example.com"],
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.organizer.user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(
|
||||
emailsDisabledSetup.organizer.user.id,
|
||||
emailsDisabledSetup.organizer.user.email
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.organizer.user.email);
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.unrelatedUser.user.email);
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.guest.user.email);
|
||||
|
||||
await bookingsRepositoryFixture.deleteAllBookings(
|
||||
testSetup.organizer.user.id,
|
||||
testSetup.organizer.user.email
|
||||
);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data !== null && "id" in data;
|
||||
}
|
||||
|
||||
function verifyAddGuestsResponse(
|
||||
responseBody: AddGuestsOutput_2024_08_13,
|
||||
expectedGuestEmails: string[],
|
||||
shouldEmailsBeSent: boolean
|
||||
): BookingOutput_2024_08_13 {
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
if (!responseDataIsBooking(responseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
|
||||
const bookingData = responseBody.data;
|
||||
|
||||
expect(bookingData.guests).toBeDefined();
|
||||
const guestEmails = bookingData.guests;
|
||||
|
||||
expectedGuestEmails.forEach((email) => {
|
||||
expect(guestEmails).toContain(email);
|
||||
});
|
||||
|
||||
expect(bookingData.guests).toBeDefined();
|
||||
expectedGuestEmails.forEach((email) => {
|
||||
expect(bookingData.guests).toContain(email);
|
||||
});
|
||||
|
||||
if (shouldEmailsBeSent) {
|
||||
expect(attendeeAddGuestsEmailSpy).toHaveBeenCalled();
|
||||
expect(organizerAddGuestsEmailSpy).toHaveBeenCalled();
|
||||
expect(attendeeScheduledEmailSpy).toHaveBeenCalled();
|
||||
} else {
|
||||
expect(attendeeAddGuestsEmailSpy).not.toHaveBeenCalled();
|
||||
expect(organizerAddGuestsEmailSpy).not.toHaveBeenCalled();
|
||||
expect(attendeeScheduledEmailSpy).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
return bookingData;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { GetBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
|
||||
export class AddGuestsOutput_2024_08_13 extends GetBookingOutput_2024_08_13 {}
|
||||
+2
-1
@@ -9,7 +9,7 @@ export class BookingReferencesRepository_2024_08_13 {
|
||||
constructor(private readonly dbRead: PrismaReadService) {}
|
||||
|
||||
async getBookingReferences(bookingId: number, filter?: BookingReferencesFilterInput_2024_08_13) {
|
||||
const whereClause: Prisma.BookingReferenceWhereInput = { bookingId };
|
||||
const whereClause: Prisma.BookingReferenceWhereInput = { bookingId, deleted: null };
|
||||
|
||||
if (filter?.type) {
|
||||
whereClause.type = filter.type;
|
||||
@@ -30,6 +30,7 @@ export class BookingReferencesRepository_2024_08_13 {
|
||||
return this.dbRead.prisma.bookingReference.findFirst({
|
||||
where: {
|
||||
uid: eventUid,
|
||||
deleted: null,
|
||||
},
|
||||
include: {
|
||||
credential: true,
|
||||
@@ -0,0 +1,46 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { Injectable, Logger, HttpException, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { addGuestsHandler } from "@calcom/platform-libraries/bookings";
|
||||
import type { AddGuestsInput_2024_08_13 } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class BookingGuestsService_2024_08_13 {
|
||||
private readonly logger = new Logger("BookingGuestsService_2024_08_13");
|
||||
|
||||
constructor(
|
||||
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||
private readonly bookingsService: BookingsService_2024_08_13,
|
||||
private readonly platformBookingsService: PlatformBookingsService
|
||||
) {}
|
||||
|
||||
async addGuests(bookingUid: string, input: AddGuestsInput_2024_08_13, user: ApiAuthGuardUser) {
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
|
||||
}
|
||||
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
|
||||
const res = await addGuestsHandler({
|
||||
ctx: { user },
|
||||
input: { bookingId: booking.id, guests: input.guests },
|
||||
emailsEnabled,
|
||||
});
|
||||
if (res.message === "Guests added") {
|
||||
return await this.bookingsService.getBooking(bookingUid, user);
|
||||
} else {
|
||||
throw new HttpException(
|
||||
"Failed to add guests to the booking",
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
|
||||
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
||||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
|
||||
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
|
||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import {
|
||||
eventTypeBookingFieldsSchema,
|
||||
EventTypeWithOwnerAndTeam,
|
||||
@@ -554,7 +554,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
isRescheduleSeatedBody(body: RescheduleBookingInput): body is RescheduleSeatedBookingInput_2024_08_13 {
|
||||
return body.hasOwnProperty("seatUid");
|
||||
return Object.prototype.hasOwnProperty.call(body, "seatUid");
|
||||
}
|
||||
|
||||
async transformInputRescheduleSeatedBooking(
|
||||
@@ -770,7 +770,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
isCancelSeatedBody(body: CancelBookingInput): body is CancelSeatedBookingInput_2024_08_13 {
|
||||
return body.hasOwnProperty("seatUid");
|
||||
return Object.prototype.hasOwnProperty.call(body, "seatUid");
|
||||
}
|
||||
|
||||
async transformInputCancelBooking(bookingUid: string, inputBooking: CancelBookingInput_2024_08_13) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import {
|
||||
defaultBookingMetadata,
|
||||
defaultBookingResponses,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
|
||||
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
|
||||
|
||||
+3
-1
@@ -39,9 +39,11 @@ 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;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const { eventType: eventTypeCreated } = await createEventType({
|
||||
input: rest,
|
||||
ctx: {
|
||||
|
||||
@@ -34,6 +34,10 @@ export type Environment = {
|
||||
IS_TEAM_BILLING_ENABLED: boolean;
|
||||
// Used to enable/disable the rewrite of /api/v2 to /v2, active by default.
|
||||
REWRITE_API_V2_PREFIX: string;
|
||||
DATABASE_READ_POOL_MAX: number;
|
||||
DATABASE_WRITE_POOL_MAX: number;
|
||||
DATABASE_READ_WORKER_POOL_MAX: number;
|
||||
DATABASE_WRITE_WORKER_POOL_MAX: number;
|
||||
};
|
||||
|
||||
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
||||
|
||||
@@ -16,12 +16,12 @@ import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
||||
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { PrismaWorkerModule } from "@/modules/prisma/prisma-worker.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaWorkerModule],
|
||||
providers: [
|
||||
PrismaOOORepository,
|
||||
PrismaScheduleRepository,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { BookingCancelService } from "@/lib/services/booking-cancel.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [BookingCancelService],
|
||||
exports: [BookingCancelService],
|
||||
})
|
||||
export class BookingCancelModule {}
|
||||
@@ -7,6 +7,7 @@ import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { HashedLinkService } from "@/lib/services/hashed-link.service";
|
||||
import { LuckyUserService } from "@/lib/services/lucky-user.service";
|
||||
import { RegularBookingService } from "@/lib/services/regular-booking.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -24,9 +25,10 @@ import { Module } from "@nestjs/common";
|
||||
CacheService,
|
||||
CheckBookingAndDurationLimitsService,
|
||||
CheckBookingLimitsService,
|
||||
HashedLinkService,
|
||||
LuckyUserService,
|
||||
RegularBookingService,
|
||||
],
|
||||
exports: [RegularBookingService],
|
||||
})
|
||||
export class RegularBookingModule {}
|
||||
export class RegularBookingModule { }
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { BookingCancelService as BaseBookingCancelService } from "@calcom/features/bookings/lib/handleCancelBooking";
|
||||
|
||||
@Injectable()
|
||||
export class BookingCancelService extends BaseBookingCancelService {
|
||||
constructor(prismaWriteService: PrismaWriteService) {
|
||||
super({
|
||||
prismaClient: prismaWriteService.prisma,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { HashedLinkService as BaseHashedLinkService } from "@calcom/platform-libraries/private-links";
|
||||
|
||||
@Injectable()
|
||||
export class HashedLinkService extends BaseHashedLinkService {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
import { PrismaAttributeRepository } from "@/lib/repositories/prisma-attribute.repository";
|
||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||
import { PrismaHostRepository } from "@/lib/repositories/prisma-host.repository";
|
||||
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { HashedLinkService } from "@/lib/services/hashed-link.service";
|
||||
import { LuckyUserService } from "@/lib/services/lucky-user.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
@@ -21,26 +17,18 @@ export class RegularBookingService extends BaseRegularBookingService {
|
||||
checkBookingAndDurationLimitsService: CheckBookingAndDurationLimitsService,
|
||||
prismaWriteService: PrismaWriteService,
|
||||
bookingRepository: PrismaBookingRepository,
|
||||
featuresRepository: PrismaFeaturesRepository,
|
||||
checkBookingLimitsService: CheckBookingLimitsService,
|
||||
hashedLinkService: HashedLinkService,
|
||||
luckyUserService: LuckyUserService,
|
||||
hostRepository: PrismaHostRepository,
|
||||
oooRepository: PrismaOOORepository,
|
||||
userRepository: PrismaUserRepository,
|
||||
attributeRepository: PrismaAttributeRepository
|
||||
userRepository: PrismaUserRepository
|
||||
) {
|
||||
super({
|
||||
cacheService,
|
||||
checkBookingAndDurationLimitsService,
|
||||
prismaClient: prismaWriteService.prisma as unknown as PrismaClient,
|
||||
bookingRepository,
|
||||
featuresRepository,
|
||||
checkBookingLimitsService,
|
||||
hashedLinkService,
|
||||
luckyUserService,
|
||||
hostRepository,
|
||||
oooRepository,
|
||||
userRepository,
|
||||
attributeRepository,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,10 +286,9 @@ export class EventTypesAtomService {
|
||||
.filter(({ ...app }) => app.slug === slug)
|
||||
.map(
|
||||
async ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
credentials: _,
|
||||
credential,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
key: _2 /* don't leak to frontend */,
|
||||
...app
|
||||
}: EnabledAppType) => {
|
||||
@@ -368,7 +367,7 @@ export class EventTypesAtomService {
|
||||
endTime: endTime.toString(),
|
||||
};
|
||||
if (!eventType) throw new NotFoundException(`Event type with uid ${uid} not found`);
|
||||
if (eventType.users.length === 0 && !!!eventType.team)
|
||||
if (eventType.users.length === 0 && !eventType.team)
|
||||
throw new NotFoundException(`No users found or no team present for event type with uid ${uid}`);
|
||||
const [user] = eventType?.users.length
|
||||
? eventType.users
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { BillingProcessor } from "@/modules/billing/billing.processor";
|
||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { BillingController } from "@/modules/billing/controllers/billing.controller";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import type { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import Stripe from "stripe";
|
||||
|
||||
import { PlatformBilling, Team } from "@calcom/prisma/client";
|
||||
|
||||
export type BillingData = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AppConfig } from "@/config/type";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor";
|
||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/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";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
|
||||
import { GoogleCalendarService as GCalService } from "@/ee/calendars/services/gcal.service";
|
||||
import { GoogleCalendarEventResponse } from "@/modules/cal-unified-calendars/pipes/get-calendar-event-details-output-pipe";
|
||||
import { calendar_v3 } from "@googleapis/calendar";
|
||||
|
||||
@@ -107,7 +107,7 @@ export class MembershipsRepository {
|
||||
},
|
||||
});
|
||||
|
||||
return userOrgAdminMemberships.map((m) => m.teamId);
|
||||
return userOrgAdminMemberships.map((m: { teamId: number }) => m.teamId);
|
||||
}
|
||||
|
||||
async createMembership(teamId: number, userId: number, role: MembershipRole, accepted: boolean) {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { MembershipsRepository } from "@/modules/memberships/memberships.reposit
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { intersectionBy } from "lodash";
|
||||
|
||||
import type { Membership } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class MembershipsService {
|
||||
constructor(private readonly membershipsRepository: MembershipsRepository) {}
|
||||
@@ -16,8 +18,8 @@ export class MembershipsService {
|
||||
const secondUserMemberships = await this.membershipsRepository.findUserMemberships(secondUserId);
|
||||
|
||||
return intersectionBy(
|
||||
firstUserMemberships.filter((m) => m.accepted),
|
||||
secondUserMemberships.filter((m) => m.accepted),
|
||||
firstUserMemberships.filter((m: Membership) => m.accepted),
|
||||
secondUserMemberships.filter((m: Membership) => m.accepted),
|
||||
"teamId"
|
||||
);
|
||||
}
|
||||
|
||||
+5
-4
@@ -31,6 +31,7 @@ import {
|
||||
Delete,
|
||||
Query,
|
||||
NotFoundException,
|
||||
ParseIntPipe,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags, ApiHeader } from "@nestjs/swagger";
|
||||
|
||||
@@ -111,7 +112,7 @@ export class OAuthClientUsersController {
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
async getUserById(
|
||||
@Param("clientId") clientId: string,
|
||||
@Param("userId") userId: number
|
||||
@Param("userId", ParseIntPipe) userId: number
|
||||
): Promise<GetManagedUserOutput> {
|
||||
const user = await this.validateManagedUserOwnership(clientId, userId);
|
||||
|
||||
@@ -127,7 +128,7 @@ export class OAuthClientUsersController {
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
async updateUser(
|
||||
@Param("clientId") clientId: string,
|
||||
@Param("userId") userId: number,
|
||||
@Param("userId", ParseIntPipe) userId: number,
|
||||
@Body() body: UpdateManagedUserInput,
|
||||
@GetOrgId() organizationId: number
|
||||
): Promise<GetManagedUserOutput> {
|
||||
@@ -153,7 +154,7 @@ export class OAuthClientUsersController {
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
async deleteUser(
|
||||
@Param("clientId") clientId: string,
|
||||
@Param("userId") userId: number
|
||||
@Param("userId", ParseIntPipe) userId: number
|
||||
): Promise<GetManagedUserOutput> {
|
||||
const user = await this.validateManagedUserOwnership(clientId, userId);
|
||||
await this.userRepository.delete(userId);
|
||||
@@ -174,7 +175,7 @@ export class OAuthClientUsersController {
|
||||
})
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
async forceRefresh(
|
||||
@Param("userId") userId: number,
|
||||
@Param("userId", ParseIntPipe) userId: number,
|
||||
@Param("clientId") oAuthClientId: string
|
||||
): Promise<KeysResponseDto> {
|
||||
this.logger.log(`Forcing new access tokens for managed user with ID ${userId}`);
|
||||
|
||||
+13
-14
@@ -434,20 +434,19 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
|
||||
const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id);
|
||||
expect(responseTeamEvent).toBeDefined();
|
||||
expect(responseTeamEvent?.hosts).toEqual([
|
||||
{
|
||||
userId: teammate1.id,
|
||||
name: teammate1.name,
|
||||
username: teammate1.username,
|
||||
avatarUrl: teammate1.avatarUrl,
|
||||
},
|
||||
{
|
||||
userId: teammate2.id,
|
||||
name: teammate2.name,
|
||||
username: teammate2.username,
|
||||
avatarUrl: teammate2.avatarUrl,
|
||||
},
|
||||
]);
|
||||
expect(responseTeamEvent?.hosts?.find((host) => host.userId === teammate1.id)).toEqual({
|
||||
userId: teammate1.id,
|
||||
name: teammate1.name,
|
||||
username: teammate1.username,
|
||||
avatarUrl: teammate1.avatarUrl,
|
||||
});
|
||||
|
||||
expect(responseTeamEvent?.hosts?.find((host) => host.userId === teammate2.id)).toEqual({
|
||||
userId: teammate2.id,
|
||||
name: teammate2.name,
|
||||
username: teammate2.username,
|
||||
avatarUrl: teammate2.avatarUrl,
|
||||
});
|
||||
|
||||
if (!responseTeamEvent) {
|
||||
throw new Error("Team event not found");
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
|
||||
import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
|
||||
-3
@@ -122,7 +122,6 @@ describe("Organizations Teams Invite Endpoints", () => {
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
@@ -131,5 +130,3 @@ describe("Organizations Teams Invite Endpoints", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
+5
-5
@@ -1,5 +1,9 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { OPTIONAL_API_KEY_HEADER, OPTIONAL_X_CAL_CLIENT_ID_HEADER, OPTIONAL_X_CAL_SECRET_KEY_HEADER } from "@/lib/docs/headers";
|
||||
import {
|
||||
OPTIONAL_API_KEY_HEADER,
|
||||
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
|
||||
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
|
||||
} from "@/lib/docs/headers";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
@@ -11,10 +15,8 @@ import { Controller, UseGuards, Post, Param, ParseIntPipe, HttpCode, HttpStatus
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
|
||||
import { CreateInviteOutputDto } from "./outputs/invite.output";
|
||||
|
||||
@Controller({
|
||||
@@ -39,5 +41,3 @@ export class OrganizationsTeamsInviteController {
|
||||
return { status: SUCCESS_STATUS, data: result };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ export class InviteDataDto {
|
||||
@ApiProperty({
|
||||
description:
|
||||
"Complete invitation URL that can be shared with prospective members. Opens the signup page with the token and redirects to getting started after signup.",
|
||||
example: "http://app.cal.com/signup?token=f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2&callbackUrl=/getting-started",
|
||||
example:
|
||||
"http://app.cal.com/signup?token=f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2&callbackUrl=/getting-started",
|
||||
})
|
||||
inviteLink!: string;
|
||||
}
|
||||
@@ -23,5 +24,3 @@ export class CreateInviteOutputDto {
|
||||
@ApiProperty({ type: InviteDataDto })
|
||||
data!: InviteDataDto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
+17
-2
@@ -404,7 +404,7 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
||||
invalidWorkflow.steps = [
|
||||
{
|
||||
stepNumber: 1,
|
||||
action: "sms_number",
|
||||
action: "cal_ai_phone_call",
|
||||
recipient: PHONE_NUMBER,
|
||||
template: REMINDER,
|
||||
verifiedPhoneId: verifiedPhoneId,
|
||||
@@ -439,6 +439,18 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
||||
html: "<p>Reminder for your event {EVENT_NAME}.</p>",
|
||||
},
|
||||
},
|
||||
{
|
||||
stepNumber: 2,
|
||||
action: "sms_attendee",
|
||||
recipient: EMAIL,
|
||||
template: REMINDER,
|
||||
phoneRequired: false,
|
||||
sender: "updatedSender",
|
||||
message: {
|
||||
subject: "Update Upcoming: {EVENT_NAME}",
|
||||
text: "Update Reminder for your event {EVENT_NAME}.</p>",
|
||||
},
|
||||
},
|
||||
];
|
||||
return request(app.getHttpServer())
|
||||
.post(`${basePath}/routing-form`)
|
||||
@@ -459,12 +471,15 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
||||
}
|
||||
|
||||
expect(responseBody.data.trigger.type).toEqual(sampleCreateWorkflowRoutingFormDto.trigger.type);
|
||||
expect(responseBody.data.steps).toHaveLength(sampleCreateWorkflowRoutingFormDto.steps.length);
|
||||
expect(responseBody.data.steps).toHaveLength(validWorkflow.steps.length);
|
||||
expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.id).toBeDefined();
|
||||
expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.sender).toEqual(
|
||||
"CalcomE2EStep1"
|
||||
);
|
||||
|
||||
expect(responseBody.data.steps.find((step) => step.stepNumber === 2)?.id).toBeDefined();
|
||||
expect(responseBody.data.steps.find((step) => step.action === "sms_attendee")).toBeDefined();
|
||||
|
||||
const trigger = sampleCreateWorkflowRoutingFormDto.trigger as OnFormSubmittedTriggerDto;
|
||||
expect(responseBody.data.trigger?.type).toEqual(trigger.type);
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
import type { Prisma, AttributeToUser, Membership, User, Profile } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsUsersRepository {
|
||||
@@ -60,7 +61,11 @@ export class OrganizationsUsersRepository {
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
return attributeToUsersWithProfile.map((attributeToUser) => attributeToUser.member.user);
|
||||
return attributeToUsersWithProfile.map(
|
||||
(
|
||||
attributeToUser: AttributeToUser & { member: Membership & { user: User & { profiles: Profile[] } } }
|
||||
) => attributeToUser.member.user
|
||||
);
|
||||
}
|
||||
|
||||
async getOrganizationUsersByEmails(
|
||||
|
||||
@@ -1,36 +1,79 @@
|
||||
import type { OnModuleDestroy, OnModuleInit } from "@nestjs/common";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { Pool } from "pg";
|
||||
|
||||
import { PrismaClient } from "@calcom/prisma/client";
|
||||
|
||||
const DB_MAX_POOL_CONNECTION = 10;
|
||||
|
||||
export interface PrismaServiceOptions {
|
||||
readUrl?: string;
|
||||
maxReadConnections?: number;
|
||||
e2e?: boolean;
|
||||
type: "main" | "worker";
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PrismaReadService implements OnModuleInit, OnModuleDestroy {
|
||||
private logger = new Logger("PrismaReadService");
|
||||
|
||||
public prisma: PrismaClient;
|
||||
public prisma!: PrismaClient;
|
||||
private pool!: Pool;
|
||||
private options!: PrismaServiceOptions;
|
||||
|
||||
constructor(readonly configService: ConfigService) {
|
||||
const dbUrl = configService.get("db.readUrl", { infer: true });
|
||||
constructor(private configService?: ConfigService) {
|
||||
if (configService) {
|
||||
// Use ConfigService defaults
|
||||
const readUrl = configService.get<string>("db.readUrl", { infer: true });
|
||||
const poolMax = parseInt(
|
||||
configService.get<number>("db.readPoolMax", { infer: true }) ?? DB_MAX_POOL_CONNECTION
|
||||
);
|
||||
const e2e = configService.get<boolean>("e2e", { infer: true }) ?? false;
|
||||
|
||||
this.prisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: {
|
||||
url: dbUrl,
|
||||
},
|
||||
},
|
||||
this.setOptions({
|
||||
readUrl,
|
||||
maxReadConnections: poolMax,
|
||||
e2e,
|
||||
type: "main",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setOptions(options: PrismaServiceOptions) {
|
||||
this.options = options;
|
||||
|
||||
const dbUrl = options.readUrl;
|
||||
const isE2E = options.e2e ?? false;
|
||||
|
||||
this.pool = new Pool({
|
||||
connectionString: dbUrl,
|
||||
max: isE2E ? 1 : options.maxReadConnections ?? DB_MAX_POOL_CONNECTION,
|
||||
idleTimeoutMillis: 300000,
|
||||
});
|
||||
|
||||
const adapter = new PrismaPg(this.pool);
|
||||
this.prisma = new PrismaClient({ adapter });
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.prisma.$connect();
|
||||
if (!this.prisma) return;
|
||||
|
||||
try {
|
||||
await this.prisma.$connect();
|
||||
this.logger.log("Connected to read database");
|
||||
} catch (error) {
|
||||
this.logger.error("Database connection failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
try {
|
||||
await this.prisma.$disconnect();
|
||||
if (this.prisma) await this.prisma.$disconnect();
|
||||
if (this.pool) await this.pool.end();
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
this.logger.error("Error disconnecting from read database", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: PrismaReadService,
|
||||
useFactory: (config: ConfigService) => {
|
||||
const service = new PrismaReadService();
|
||||
service.setOptions({
|
||||
readUrl: config.get<string>("db.readUrl", { infer: true }),
|
||||
maxReadConnections: parseInt(config.get<number>("db.workerReadPoolMax", { infer: true })),
|
||||
e2e: config.get<boolean>("e2e", { infer: true }) ?? false,
|
||||
type: "worker",
|
||||
});
|
||||
return service;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
{
|
||||
provide: PrismaWriteService,
|
||||
useFactory: (config: ConfigService) => {
|
||||
const service = new PrismaWriteService();
|
||||
service.setOptions({
|
||||
writeUrl: config.get<string>("db.writeUrl", { infer: true }),
|
||||
maxWriteConnections: parseInt(config.get<number>("db.workerWritePoolMax", { infer: true })),
|
||||
e2e: config.get<boolean>("e2e", { infer: true }) ?? false,
|
||||
type: "worker",
|
||||
});
|
||||
return service;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [PrismaReadService, PrismaWriteService],
|
||||
})
|
||||
export class PrismaWorkerModule {}
|
||||
@@ -1,36 +1,75 @@
|
||||
import { OnModuleDestroy, OnModuleInit } from "@nestjs/common";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { OnModuleDestroy, OnModuleInit, Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { Pool } from "pg";
|
||||
|
||||
import { PrismaClient } from "@calcom/prisma/client";
|
||||
|
||||
const DB_MAX_POOL_CONNECTION = 5;
|
||||
|
||||
export interface PrismaServiceOptions {
|
||||
writeUrl?: string;
|
||||
maxWriteConnections?: number;
|
||||
e2e?: boolean;
|
||||
type: "main" | "worker";
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PrismaWriteService implements OnModuleInit, OnModuleDestroy {
|
||||
private logger = new Logger("PrismaWriteService");
|
||||
public prisma!: PrismaClient;
|
||||
private pool!: Pool;
|
||||
private options!: PrismaServiceOptions;
|
||||
|
||||
public prisma: PrismaClient;
|
||||
constructor(private configService?: ConfigService) {
|
||||
if (configService) {
|
||||
const writeUrl = configService.get<string>("db.writeUrl", { infer: true });
|
||||
const poolMax = parseInt(
|
||||
configService.get<number>("db.writePoolMax", { infer: true }) ?? DB_MAX_POOL_CONNECTION
|
||||
);
|
||||
const e2e = configService.get<boolean>("e2e", { infer: true }) ?? false;
|
||||
|
||||
constructor(readonly configService: ConfigService) {
|
||||
const dbUrl = configService.get("db.writeUrl", { infer: true });
|
||||
this.setOptions({
|
||||
writeUrl,
|
||||
maxWriteConnections: poolMax,
|
||||
e2e,
|
||||
type: "main",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.prisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: {
|
||||
url: dbUrl,
|
||||
},
|
||||
},
|
||||
setOptions(options: PrismaServiceOptions) {
|
||||
this.options = options;
|
||||
|
||||
const dbUrl = options.writeUrl;
|
||||
const isE2E = options.e2e ?? false;
|
||||
this.pool = new Pool({
|
||||
connectionString: dbUrl,
|
||||
max: isE2E ? 1 : options.maxWriteConnections ?? DB_MAX_POOL_CONNECTION,
|
||||
idleTimeoutMillis: 300000,
|
||||
});
|
||||
|
||||
const adapter = new PrismaPg(this.pool);
|
||||
this.prisma = new PrismaClient({ adapter });
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.prisma.$connect();
|
||||
if (!this.prisma) return;
|
||||
|
||||
try {
|
||||
await this.prisma.$connect();
|
||||
this.logger.log("Connected to write database");
|
||||
} catch (error) {
|
||||
this.logger.error("Database connection failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
try {
|
||||
await this.prisma.$disconnect();
|
||||
if (this.prisma) await this.prisma.$disconnect();
|
||||
if (this.pool) await this.pool.end();
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
this.logger.error("Error disconnecting from write database", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,40 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
|
||||
@Module({
|
||||
providers: [PrismaReadService, PrismaWriteService],
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: PrismaReadService,
|
||||
useFactory: (config: ConfigService) => {
|
||||
const service = new PrismaReadService();
|
||||
service.setOptions({
|
||||
readUrl: config.get<string>("db.readUrl", { infer: true }),
|
||||
maxReadConnections: parseInt(config.get<number>("db.readPoolMax", { infer: true })),
|
||||
e2e: config.get<boolean>("e2e", { infer: true }) ?? false,
|
||||
type: "main",
|
||||
});
|
||||
return service;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
{
|
||||
provide: PrismaWriteService,
|
||||
useFactory: (config: ConfigService) => {
|
||||
const service = new PrismaWriteService();
|
||||
service.setOptions({
|
||||
writeUrl: config.get<string>("db.writeUrl", { infer: true }),
|
||||
maxWriteConnections: parseInt(config.get<number>("db.writePoolMax", { infer: true })),
|
||||
e2e: config.get<boolean>("e2e", { infer: true }) ?? false,
|
||||
type: "main",
|
||||
});
|
||||
return service;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [PrismaReadService, PrismaWriteService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Needed if ConfigService is used anywhere
|
||||
import appConfig from "@/config/app";
|
||||
import { AvailableSlotsModule } from "@/lib/modules/available-slots.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { PrismaWorkerModule } from "@/modules/prisma/prisma-worker.module";
|
||||
import { Module, Logger } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { SentryModule } from "@sentry/nestjs/setup";
|
||||
@@ -15,7 +15,7 @@ import { SentryModule } from "@sentry/nestjs/setup";
|
||||
isGlobal: true,
|
||||
load: [appConfig],
|
||||
}),
|
||||
PrismaModule,
|
||||
PrismaWorkerModule,
|
||||
AvailableSlotsModule,
|
||||
],
|
||||
providers: [Logger],
|
||||
|
||||
@@ -118,7 +118,7 @@ export class StripeService {
|
||||
userId
|
||||
);
|
||||
|
||||
const credentialIdsToDelete = existingCredentials.map((item) => item.id);
|
||||
const credentialIdsToDelete = existingCredentials.map((item: Credential) => item.id);
|
||||
if (credentialIdsToDelete.length > 0) {
|
||||
await this.appsRepository.deleteAppCredentials(credentialIdsToDelete, userId);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export const MembershipUserSelect: Prisma.UserSelect = {
|
||||
name: true,
|
||||
metadata: true,
|
||||
bio: true,
|
||||
};
|
||||
} satisfies Prisma.UserSelect;
|
||||
|
||||
@Injectable()
|
||||
export class TeamsMembershipsRepository {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { teamMetadataSchema } from "@calcom/platform-libraries";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
import type { Membership, Prisma } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class TeamsRepository {
|
||||
@@ -41,7 +41,7 @@ export class TeamsRepository {
|
||||
return [];
|
||||
}
|
||||
|
||||
return teamMembers.map((member) => member.userId);
|
||||
return teamMembers.map((member: Membership) => member.userId);
|
||||
}
|
||||
|
||||
async getTeamManagedUsersIds(teamId: number) {
|
||||
@@ -57,7 +57,7 @@ export class TeamsRepository {
|
||||
return [];
|
||||
}
|
||||
|
||||
return teamMembers.map((member) => member.userId);
|
||||
return teamMembers.map((member: Membership) => member.userId);
|
||||
}
|
||||
|
||||
async getTeamsUserIsMemberOf(userId: number) {
|
||||
|
||||
@@ -289,7 +289,7 @@ export class UsersRepository {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
return profiles.map((profile) => profile.user);
|
||||
return profiles.map((profile: Profile & { user: User }) => profile.user);
|
||||
}
|
||||
|
||||
async setDefaultConferencingApp(userId: number, appSlug?: string, appLink?: string) {
|
||||
|
||||
@@ -7,8 +7,12 @@ import {
|
||||
EMAIL_ADDRESS,
|
||||
EMAIL_ATTENDEE,
|
||||
FORM_ALLOWED_STEP_ACTIONS,
|
||||
SMS_NUMBER,
|
||||
WorkflowEmailAddressStepDto,
|
||||
WorkflowEmailAttendeeStepDto,
|
||||
WorkflowPhoneNumberStepDto,
|
||||
WorkflowPhoneAttendeeStepDto,
|
||||
SMS_ATTENDEE,
|
||||
} from "./workflow-step.input";
|
||||
import {
|
||||
RoutingFormWorkflowTriggerDto,
|
||||
@@ -85,6 +89,8 @@ export class CreateFormWorkflowDto {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(WorkflowEmailAddressStepDto) },
|
||||
{ $ref: getSchemaPath(WorkflowEmailAttendeeStepDto) },
|
||||
{ $ref: getSchemaPath(WorkflowPhoneAttendeeStepDto) },
|
||||
{ $ref: getSchemaPath(WorkflowPhoneNumberStepDto) },
|
||||
],
|
||||
type: "array",
|
||||
})
|
||||
@@ -99,8 +105,15 @@ export class CreateFormWorkflowDto {
|
||||
subTypes: [
|
||||
{ value: WorkflowEmailAddressStepDto, name: EMAIL_ADDRESS },
|
||||
{ value: WorkflowEmailAttendeeStepDto, name: EMAIL_ATTENDEE },
|
||||
{ value: WorkflowPhoneAttendeeStepDto, name: SMS_ATTENDEE },
|
||||
{ value: WorkflowPhoneNumberStepDto, name: SMS_NUMBER },
|
||||
],
|
||||
},
|
||||
})
|
||||
steps!: (WorkflowEmailAddressStepDto | WorkflowEmailAttendeeStepDto)[];
|
||||
steps!: (
|
||||
| WorkflowEmailAddressStepDto
|
||||
| WorkflowEmailAttendeeStepDto
|
||||
| WorkflowPhoneAttendeeStepDto
|
||||
| WorkflowPhoneNumberStepDto
|
||||
)[];
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
EMAIL_ADDRESS,
|
||||
EMAIL_ATTENDEE,
|
||||
FORM_ALLOWED_STEP_ACTIONS,
|
||||
SMS_ATTENDEE,
|
||||
SMS_NUMBER,
|
||||
UpdateEmailAddressWorkflowStepDto,
|
||||
UpdateEmailAttendeeWorkflowStepDto,
|
||||
UpdateEmailHostWorkflowStepDto,
|
||||
@@ -75,6 +77,8 @@ export class UpdateFormWorkflowDto {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(UpdateEmailAddressWorkflowStepDto) },
|
||||
{ $ref: getSchemaPath(UpdateEmailAttendeeWorkflowStepDto) },
|
||||
{ $ref: getSchemaPath(UpdatePhoneAttendeeWorkflowStepDto) },
|
||||
{ $ref: getSchemaPath(UpdatePhoneNumberWorkflowStepDto) },
|
||||
],
|
||||
type: "array",
|
||||
})
|
||||
@@ -90,8 +94,15 @@ export class UpdateFormWorkflowDto {
|
||||
subTypes: [
|
||||
{ value: UpdateEmailAddressWorkflowStepDto, name: EMAIL_ADDRESS },
|
||||
{ value: UpdateEmailAttendeeWorkflowStepDto, name: EMAIL_ATTENDEE },
|
||||
{ value: UpdatePhoneAttendeeWorkflowStepDto, name: SMS_ATTENDEE },
|
||||
{ value: UpdatePhoneNumberWorkflowStepDto, name: SMS_NUMBER },
|
||||
],
|
||||
},
|
||||
})
|
||||
steps?: (UpdateEmailAddressWorkflowStepDto | UpdateEmailAttendeeWorkflowStepDto)[];
|
||||
steps?: (
|
||||
| UpdateEmailAddressWorkflowStepDto
|
||||
| UpdateEmailAttendeeWorkflowStepDto
|
||||
| UpdatePhoneNumberWorkflowStepDto
|
||||
| UpdatePhoneAttendeeWorkflowStepDto
|
||||
)[];
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export const STEP_ACTIONS = [
|
||||
CAL_AI_PHONE_CALL,
|
||||
] as const;
|
||||
|
||||
export const FORM_ALLOWED_STEP_ACTIONS = [EMAIL_ATTENDEE, EMAIL_ADDRESS] as const;
|
||||
export const FORM_ALLOWED_STEP_ACTIONS = [EMAIL_ATTENDEE, EMAIL_ADDRESS, SMS_ATTENDEE, SMS_NUMBER] as const;
|
||||
|
||||
export const STEP_ACTIONS_TO_ENUM = {
|
||||
[EMAIL_HOST]: WorkflowActions.EMAIL_HOST,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@prisma/client/*": ["./client/*"],
|
||||
"@calcom/prisma/client": ["../../../packages/prisma/generated/prisma/client"],
|
||||
"@calcom/prisma/enums": ["../../../packages/prisma/enums"],
|
||||
"@calcom/platform-constants": ["../../../packages/platform/constants/index.ts"],
|
||||
"@calcom/platform-types": ["../../../packages/platform/types/index.ts"],
|
||||
|
||||
@@ -3,6 +3,7 @@ import { unstable_cache } from "next/cache";
|
||||
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
|
||||
import { getTeamData } from "@calcom/features/ee/teams/lib/getTeamData";
|
||||
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||
import {
|
||||
getEventTypeHosts,
|
||||
getProfileFromEvent,
|
||||
@@ -11,10 +12,9 @@ import {
|
||||
} from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||
import { getTeamEventType } from "@calcom/features/eventtypes/lib/getTeamEventType";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
@@ -12,7 +12,7 @@ import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { validStatuses } from "~/bookings/lib/validStatuses";
|
||||
import BookingsList from "~/bookings/views/bookings-listing-view";
|
||||
import BookingsList from "~/bookings/views/bookings-view";
|
||||
|
||||
const querySchema = z.object({
|
||||
status: z.enum(validStatuses),
|
||||
|
||||
@@ -23,8 +23,14 @@ const ServerPage = async ({ searchParams: _searchParams }: ServerPageProps) => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
const searchParams = await _searchParams;
|
||||
const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token;
|
||||
const autoAccept = Array.isArray(searchParams?.autoAccept) ? searchParams.autoAccept[0] : searchParams?.autoAccept;
|
||||
const callbackUrl = token ? `/teams?token=${encodeURIComponent(token)}${autoAccept ? `&autoAccept=${encodeURIComponent(autoAccept)}` : ''}` : null;
|
||||
const autoAccept = Array.isArray(searchParams?.autoAccept)
|
||||
? searchParams.autoAccept[0]
|
||||
: searchParams?.autoAccept;
|
||||
const callbackUrl = token
|
||||
? `/teams?token=${encodeURIComponent(token)}${
|
||||
autoAccept ? `&autoAccept=${encodeURIComponent(autoAccept)}` : ""
|
||||
}`
|
||||
: null;
|
||||
|
||||
if (!session) {
|
||||
redirect(callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}` : "/auth/login");
|
||||
|
||||
@@ -3,9 +3,9 @@ import type { Session } from "next-auth";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||
import { TeamService } from "@calcom/features/ee/teams/services/teamService";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
@@ -33,7 +33,9 @@ export const ServerTeamsListing = async ({
|
||||
session: Session;
|
||||
}) => {
|
||||
const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token;
|
||||
const autoAccept = Array.isArray(searchParams?.autoAccept) ? searchParams.autoAccept[0] : searchParams?.autoAccept;
|
||||
const autoAccept = Array.isArray(searchParams?.autoAccept)
|
||||
? searchParams.autoAccept[0]
|
||||
: searchParams?.autoAccept;
|
||||
const userId = session.user.id;
|
||||
let invitationAccepted = false;
|
||||
|
||||
@@ -42,7 +44,7 @@ export const ServerTeamsListing = async ({
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
if (autoAccept === "true") {
|
||||
if (autoAccept === "true") {
|
||||
await TeamService.acceptInvitationByToken(token, userId);
|
||||
invitationAccepted = true;
|
||||
} else {
|
||||
|
||||
@@ -308,7 +308,7 @@ const WeightedAttributesSelector = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-muted mt-1 rounded-xl p-2">
|
||||
{!!attributeIdForWeights ? (
|
||||
{attributeIdForWeights ? (
|
||||
<SelectField
|
||||
size="sm"
|
||||
containerClassName="data-testid-select-router"
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { isFallbackRoute } from "@calcom/app-store/routing-forms/lib/isFallbackRoute";
|
||||
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { useHasPaidPlan } from "@calcom/features/billing/hooks/useHasPaidPlan";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import SkeletonLoaderTeamList from "@calcom/features/ee/teams/components/SkeletonloaderTeamList";
|
||||
import { CreateButtonWithTeamsList } from "@calcom/features/ee/teams/components/createButton/CreateButtonWithTeamsList";
|
||||
@@ -15,7 +16,6 @@ import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsF
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
import { UpgradeTip } from "@calcom/features/tips";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
@@ -76,7 +76,6 @@ export default function RoutingForms({ appUrl }: { appUrl: string }) {
|
||||
|
||||
useEffect(() => {
|
||||
hookForm.reset({});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const filters = getTeamsFiltersFromQuery(routerQuery);
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { OnboardingView } from "~/onboarding/getting-started/onboarding-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${APP_NAME} - ${t("getting_started")}`,
|
||||
() => "",
|
||||
true,
|
||||
undefined,
|
||||
"/onboarding/getting-started"
|
||||
);
|
||||
};
|
||||
|
||||
const ServerPage = async () => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
// Hello {username} || there. Not sure how to do this nicely with i18n just yet
|
||||
const userName = session.user.name || "there";
|
||||
const userEmail = session.user.email || "";
|
||||
|
||||
return <OnboardingView userName={userName} userEmail={userEmail} />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
export default async function OnboardingLayout({ children }: { children: React.ReactNode }) {
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const isOnboardingV3Enabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("onboarding-v3");
|
||||
|
||||
if (!isOnboardingV3Enabled) {
|
||||
redirect("/getting-started");
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { OrganizationBrandView } from "~/onboarding/organization/brand/organization-brand-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${APP_NAME} - ${t("organization_brand")}`,
|
||||
() => "",
|
||||
true,
|
||||
undefined,
|
||||
"/onboarding/organization/brand"
|
||||
);
|
||||
};
|
||||
|
||||
const ServerPage = async () => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const userEmail = session.user.email || "";
|
||||
|
||||
return <OrganizationBrandView userEmail={userEmail} />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { OrganizationDetailsView } from "~/onboarding/organization/details/organization-details-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${APP_NAME} - ${t("organization_details")}`,
|
||||
() => "",
|
||||
true,
|
||||
undefined,
|
||||
"/onboarding/organization/details"
|
||||
);
|
||||
};
|
||||
|
||||
const ServerPage = async () => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const userEmail = session.user.email || "";
|
||||
|
||||
return <OrganizationDetailsView userEmail={userEmail} />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user