Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87d2ec9e75 | ||
|
|
226686d4dd | ||
|
|
518ab82e3b | ||
|
|
532b78a9b3 | ||
|
|
1251091300 | ||
|
|
ce1459011a | ||
|
|
e1bdac077a | ||
|
|
d666f2926c | ||
|
|
745f0b0872 | ||
|
|
65f50e9f25 | ||
|
|
54162f8568 | ||
|
|
0f4b9b2a6e | ||
|
|
70b4e3a9c6 | ||
|
|
ab44d5b92c | ||
|
|
bed08c1e4f | ||
|
|
1e0f7f433f | ||
|
|
0cc7a9fc22 | ||
|
|
b03b878199 | ||
|
|
7a560cf09b | ||
|
|
58ad441223 | ||
|
|
d8de919acb | ||
|
|
d699b33dcb | ||
|
|
c071df4cd6 | ||
|
|
219b13d053 | ||
|
|
5e09281974 | ||
|
|
b66d36a98e | ||
|
|
68ce9524eb | ||
|
|
357a42b512 | ||
|
|
33d7da88bf | ||
|
|
cd888be61e | ||
|
|
06231ec299 | ||
|
|
e38705c3e8 | ||
|
|
5c95268d1a | ||
|
|
b310eb1a7e | ||
|
|
051510fa36 | ||
|
|
7d1226b823 | ||
|
|
7732a22c49 | ||
|
|
2a41cd0770 | ||
|
|
a574e5e60f | ||
|
|
0b6508379b | ||
|
|
f6930df87d | ||
|
|
7d4e8e2210 | ||
|
|
50eeb712e3 | ||
|
|
75edc4539c | ||
|
|
def87ea84a | ||
|
|
4153e5b2d5 | ||
|
|
8c2f6ae145 | ||
|
|
78ea4d2f68 | ||
|
|
e7c638c1e4 | ||
|
|
c28fab4fe2 | ||
|
|
e8a02554b6 | ||
|
|
4017c0e28f | ||
|
|
d72f7f8a42 | ||
|
|
3a4ebadbf3 | ||
|
|
765d601ff9 | ||
|
|
3a22fdb428 | ||
|
|
93580ac4bc | ||
|
|
b15ad6f233 | ||
|
|
56e59ab3e1 | ||
|
|
ee796933ee | ||
|
|
d2f76edfd6 | ||
|
|
822f0204a6 | ||
|
|
9770aff153 | ||
|
|
723d0ddec2 | ||
|
|
60b61eb625 | ||
|
|
5d2d99006b | ||
|
|
5d01eb2f38 | ||
|
|
ac3fb0a1cd | ||
|
|
1730ef7d5d | ||
|
|
9e371ded90 |
No files matched your search
@@ -19,6 +19,11 @@
|
|||||||
# instructions. This environment variable is deprecated although still supported for backward compatibility.
|
# instructions. This environment variable is deprecated although still supported for backward compatibility.
|
||||||
# @see https://console.cal.com
|
# @see https://console.cal.com
|
||||||
CALCOM_LICENSE_KEY=
|
CALCOM_LICENSE_KEY=
|
||||||
|
# Signature token for the Cal.com License API (used for self-hosted integrations)
|
||||||
|
# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key
|
||||||
|
CAL_SIGNATURE_TOKEN=
|
||||||
|
# The route to the Cal.com License API
|
||||||
|
CALCOM_PRIVATE_API_ROUTE=
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
# - DATABASE ************************************************************************************************
|
# - DATABASE ************************************************************************************************
|
||||||
|
|||||||
@@ -37,12 +37,3 @@ jobs:
|
|||||||
```
|
```
|
||||||
${{ steps.lint_pr_title.outputs.error_message }}
|
${{ steps.lint_pr_title.outputs.error_message }}
|
||||||
```
|
```
|
||||||
|
|
||||||
# Delete a previous comment when the issue has been resolved
|
|
||||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
|
||||||
uses: marocchino/sticky-pull-request-comment@v2
|
|
||||||
with:
|
|
||||||
header: pr-title-lint-error
|
|
||||||
message: |
|
|
||||||
Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link.
|
|
||||||
|
|
||||||
+1
-1
@@ -99,7 +99,7 @@ To develop locally:
|
|||||||
|
|
||||||
- Duplicate `.env.example` to `.env`.
|
- Duplicate `.env.example` to `.env`.
|
||||||
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
||||||
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
- Use `openssl rand -base64 32` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||||
|
|
||||||
6. Setup Node
|
6. Setup Node
|
||||||
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
export function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
// reduce sample rate to 10% on production
|
||||||
|
tracesSampleRate: process.env.NODE_ENV !== "production" ? 1.0 : 0.1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NEXT_RUNTIME === "edge") {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,3 +14,7 @@ export const withValidQueryIdTransformParseInt = withValidation({
|
|||||||
type: "Zod",
|
type: "Zod",
|
||||||
mode: "query",
|
mode: "query",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getTranscriptFromRecordingId = schemaQueryIdParseInt.extend({
|
||||||
|
recordingId: z.string(),
|
||||||
|
});
|
||||||
@@ -2,7 +2,12 @@ const { withAxiom } = require("next-axiom");
|
|||||||
const { withSentryConfig } = require("@sentry/nextjs");
|
const { withSentryConfig } = require("@sentry/nextjs");
|
||||||
|
|
||||||
const plugins = [withAxiom];
|
const plugins = [withAxiom];
|
||||||
|
|
||||||
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
experimental: {
|
||||||
|
instrumentationHook: true,
|
||||||
|
},
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
"@calcom/app-store",
|
"@calcom/app-store",
|
||||||
"@calcom/core",
|
"@calcom/core",
|
||||||
@@ -87,12 +92,12 @@ const nextConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||||
nextConfig["sentry"] = {
|
plugins.push((nextConfig) =>
|
||||||
autoInstrumentServerFunctions: true,
|
withSentryConfig(nextConfig, {
|
||||||
hideSourceMaps: true,
|
autoInstrumentServerFunctions: true,
|
||||||
};
|
hideSourceMaps: true,
|
||||||
|
})
|
||||||
plugins.push(withSentryConfig);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"@calcom/lib": "*",
|
"@calcom/lib": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "*",
|
||||||
"@sentry/nextjs": "^7.73.0",
|
"@sentry/nextjs": "^8.8.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"next": "^13.5.4",
|
"next": "^13.5.4",
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getTranscriptsAccessLinkFromRecordingId,
|
||||||
|
checkIfRoomNameMatchesInRecording,
|
||||||
|
} from "@calcom/core/videoClient";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import type { PartialReference } from "@calcom/types/EventManager";
|
||||||
|
|
||||||
|
import { getTranscriptFromRecordingId } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /bookings/{id}/transcripts/{recordingId}:
|
||||||
|
* get:
|
||||||
|
* summary: Find all Cal video transcripts of that recording
|
||||||
|
* operationId: getTranscriptsByRecordingId
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* required: true
|
||||||
|
* description: ID of the booking for which transcripts need to be fetched.
|
||||||
|
* - in: path
|
||||||
|
* name: recordingId
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* required: true
|
||||||
|
* description: ID of the recording(daily.co recording id) for which transcripts need to be fetched.
|
||||||
|
* - in: query
|
||||||
|
* name: apiKey
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Your API key
|
||||||
|
* tags:
|
||||||
|
* - bookings
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* 401:
|
||||||
|
* description: Authorization information is missing or invalid.
|
||||||
|
* 404:
|
||||||
|
* description: Booking was not found
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function getHandler(req: NextApiRequest) {
|
||||||
|
const { query } = req;
|
||||||
|
const { id, recordingId } = getTranscriptFromRecordingId.parse(query);
|
||||||
|
|
||||||
|
await checkIfRecordingBelongsToBooking(id, recordingId);
|
||||||
|
|
||||||
|
const transcriptsAccessLinks = await getTranscriptsAccessLinkFromRecordingId(recordingId);
|
||||||
|
|
||||||
|
return transcriptsAccessLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkIfRecordingBelongsToBooking = async (bookingId: number, recordingId: string) => {
|
||||||
|
const booking = await prisma.booking.findUnique({
|
||||||
|
where: { id: bookingId },
|
||||||
|
include: { references: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!booking)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: `No Booking found with booking id ${bookingId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const roomName =
|
||||||
|
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
|
||||||
|
undefined;
|
||||||
|
|
||||||
|
if (!roomName)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: `No Booking Reference with Daily Video found with booking id ${bookingId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const canUserAccessRecordingId = await checkIfRoomNameMatchesInRecording(roomName, recordingId);
|
||||||
|
if (!canUserAccessRecordingId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: `This Recording Id ${recordingId} does not belong to booking ${bookingId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defaultResponder(getHandler);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
|
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||||
|
|
||||||
|
import authMiddleware from "../../_auth-middleware";
|
||||||
|
|
||||||
|
export default withMiddleware()(
|
||||||
|
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
await authMiddleware(req);
|
||||||
|
return defaultHandler({
|
||||||
|
GET: import("./_get"),
|
||||||
|
})(req, res);
|
||||||
|
})
|
||||||
|
);
|
||||||
@@ -7,7 +7,12 @@ import { schemaUserReadPublic } from "~/lib/validations/user";
|
|||||||
|
|
||||||
async function handler({ userId }: NextApiRequest) {
|
async function handler({ userId }: NextApiRequest) {
|
||||||
const data = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
|
const data = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
|
||||||
return { user: schemaUserReadPublic.parse(data) };
|
return {
|
||||||
|
user: schemaUserReadPublic.parse({
|
||||||
|
...data,
|
||||||
|
avatar: data.avatarUrl,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultResponder(handler);
|
export default defaultResponder(handler);
|
||||||
@@ -45,7 +45,10 @@ export async function getHandler(req: NextApiRequest) {
|
|||||||
if (!isSystemWideAdmin && query.userId !== req.userId)
|
if (!isSystemWideAdmin && query.userId !== req.userId)
|
||||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
const data = await prisma.user.findUnique({ where: { id: query.userId } });
|
const data = await prisma.user.findUnique({ where: { id: query.userId } });
|
||||||
const user = schemaUserReadPublic.parse(data);
|
const user = schemaUserReadPublic.parse({
|
||||||
|
...data,
|
||||||
|
avatar: data?.avatarUrl,
|
||||||
|
});
|
||||||
return { user };
|
return { user };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import type { NextApiRequest } from "next";
|
|||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import { uploadAvatar } from "@calcom/lib/server/avatar";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId";
|
import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId";
|
||||||
import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validations/user";
|
import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validations/user";
|
||||||
@@ -101,7 +103,8 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
if (!isSystemWideAdmin && query.userId !== req.userId)
|
if (!isSystemWideAdmin && query.userId !== req.userId)
|
||||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
|
|
||||||
const body = await schemaUserEditBodyParams.parseAsync(req.body);
|
const { avatar, ...body }: { avatar?: string | undefined } & Prisma.UserUpdateInput =
|
||||||
|
await schemaUserEditBodyParams.parseAsync(req.body);
|
||||||
// disable role or branding changes unless admin.
|
// disable role or branding changes unless admin.
|
||||||
if (!isSystemWideAdmin) {
|
if (!isSystemWideAdmin) {
|
||||||
if (body.role) body.role = undefined;
|
if (body.role) body.role = undefined;
|
||||||
@@ -119,6 +122,14 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
message: "Bad request: Invalid default schedule id",
|
message: "Bad request: Invalid default schedule id",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (avatar) {
|
||||||
|
body.avatarUrl = await uploadAvatar({
|
||||||
|
userId: query.userId,
|
||||||
|
avatar: await (await import("@calcom/lib/server/resizeBase64Image")).resizeBase64Image(avatar),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const data = await prisma.user.update({
|
const data = await prisma.user.update({
|
||||||
where: { id: query.userId },
|
where: { id: query.userId },
|
||||||
data: body,
|
data: body,
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { init as SentryInit } from "@sentry/nextjs";
|
|
||||||
|
|
||||||
SentryInit({
|
|
||||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { init as SentryInit } from "@sentry/nextjs";
|
|
||||||
|
|
||||||
SentryInit({
|
|
||||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
||||||
tracesSampleRate: 1.0,
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import prismaMock from "../../../../../../../../../tests/libs/__mocks__/prismaMock";
|
||||||
|
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getTranscriptsAccessLinkFromRecordingId,
|
||||||
|
checkIfRoomNameMatchesInRecording,
|
||||||
|
} from "@calcom/core/videoClient";
|
||||||
|
import { buildBooking } from "@calcom/lib/test/builder";
|
||||||
|
|
||||||
|
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||||
|
|
||||||
|
import authMiddleware from "../../../../../../pages/api/bookings/[id]/_auth-middleware";
|
||||||
|
import handler from "../../../../../../pages/api/bookings/[id]/transcripts/[recordingId]/_get";
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
vi.mock("@calcom/core/videoClient", () => {
|
||||||
|
return {
|
||||||
|
getTranscriptsAccessLinkFromRecordingId: vi.fn(),
|
||||||
|
checkIfRoomNameMatchesInRecording: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("~/lib/utils/retrieveScopedAccessibleUsers", () => {
|
||||||
|
return {
|
||||||
|
getAccessibleUsers: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockGetTranscripts = () => {
|
||||||
|
const downloadLinks = [{ format: "json", link: "https://URL1" }];
|
||||||
|
|
||||||
|
vi.mocked(getTranscriptsAccessLinkFromRecordingId).mockResolvedValue(downloadLinks);
|
||||||
|
vi.mocked(checkIfRoomNameMatchesInRecording).mockResolvedValue(true);
|
||||||
|
|
||||||
|
return downloadLinks;
|
||||||
|
};
|
||||||
|
|
||||||
|
const recordingId = "abc-xyz";
|
||||||
|
|
||||||
|
describe("GET /api/bookings/[id]/transcripts/[recordingId]", () => {
|
||||||
|
test("Returns transcripts if user is system-wide admin", async () => {
|
||||||
|
const adminUserId = 1;
|
||||||
|
const userId = 2;
|
||||||
|
|
||||||
|
const bookingId = 1111;
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(
|
||||||
|
buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId,
|
||||||
|
references: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: "daily_video",
|
||||||
|
uid: "17OHkCH53pBa03FhxMbw",
|
||||||
|
meetingId: "17OHkCH53pBa03FhxMbw",
|
||||||
|
meetingPassword: "password",
|
||||||
|
meetingUrl: "https://URL",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const mockedTranscripts = mockGetTranscripts();
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
body: {},
|
||||||
|
query: {
|
||||||
|
id: bookingId,
|
||||||
|
recordingId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.isSystemWideAdmin = true;
|
||||||
|
req.userId = adminUserId;
|
||||||
|
|
||||||
|
await authMiddleware(req);
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(JSON.parse(res._getData())).toEqual(mockedTranscripts);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Allows GET transcripts when user is org-wide admin", async () => {
|
||||||
|
const adminUserId = 1;
|
||||||
|
const memberUserId = 10;
|
||||||
|
const bookingId = 3333;
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(
|
||||||
|
buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId: memberUserId,
|
||||||
|
references: [
|
||||||
|
{ id: 1, type: "daily_video", uid: "17OHkCH53pBa03FhxMbw", meetingId: "17OHkCH53pBa03FhxMbw" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
body: {},
|
||||||
|
query: {
|
||||||
|
id: bookingId,
|
||||||
|
recordingId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = adminUserId;
|
||||||
|
req.isOrganizationOwnerOrAdmin = true;
|
||||||
|
mockGetTranscripts();
|
||||||
|
|
||||||
|
vi.mocked(getAccessibleUsers).mockResolvedValue([memberUserId]);
|
||||||
|
|
||||||
|
await authMiddleware(req);
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,5 +2,6 @@ const rootConfig = require("../../../packages/config/prettier-preset");
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...rootConfig,
|
...rootConfig,
|
||||||
|
importOrder: ["^./instrument", ...rootConfig.importOrder],
|
||||||
importOrderParserPlugins: ["typescript", "decorators-legacy"],
|
importOrderParserPlugins: ["typescript", "decorators-legacy"],
|
||||||
};
|
};
|
||||||
@@ -25,11 +25,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-libraries": "0.0.2",
|
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||||
|
"@calcom/platform-libraries-0.0.4": "npm:@calcom/platform-libraries@0.0.4",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
"@golevelup/ts-jest": "^0.4.0",
|
"@golevelup/ts-jest": "^0.4.0",
|
||||||
|
"@microsoft/microsoft-graph-types-beta": "^0.42.0-preview",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "^3.1.1",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^10.0.0",
|
||||||
@@ -38,8 +40,7 @@
|
|||||||
"@nestjs/platform-express": "^10.0.0",
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
"@nestjs/swagger": "^7.3.0",
|
"@nestjs/swagger": "^7.3.0",
|
||||||
"@nestjs/throttler": "^5.1.2",
|
"@nestjs/throttler": "^5.1.2",
|
||||||
"@sentry/node": "^7.86.0",
|
"@sentry/node": "^8.8.0",
|
||||||
"@sentry/tracing": "^7.86.0",
|
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
"querystring": "^0.2.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"stripe": "^15.3.0",
|
"stripe": "^15.3.0",
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ import { AppController } from "./app.controller";
|
|||||||
EndpointsModule,
|
EndpointsModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
JwtModule,
|
JwtModule,
|
||||||
//register prefix for all routes in EndpointsModule
|
|
||||||
RouterModule.register([{ path: "/v2", module: EndpointsModule }]),
|
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
+36
-15
@@ -1,26 +1,43 @@
|
|||||||
import { getEnv } from "@/env";
|
import "./instrument";
|
||||||
|
|
||||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
import { SentryFilter } from "@/filters/sentry-exception.filter";
|
|
||||||
import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
|
import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
|
||||||
import type { ValidationError } from "@nestjs/common";
|
import type { ValidationError } from "@nestjs/common";
|
||||||
import { BadRequestException, ValidationPipe, VersioningType } from "@nestjs/common";
|
import { BadRequestException, ValidationPipe, VersioningType } from "@nestjs/common";
|
||||||
import { HttpAdapterHost } from "@nestjs/core";
|
import { BaseExceptionFilter, HttpAdapterHost } from "@nestjs/core";
|
||||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
import * as cookieParser from "cookie-parser";
|
import * as cookieParser from "cookie-parser";
|
||||||
|
import { Request } from "express";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
|
|
||||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
import {
|
||||||
|
API_VERSIONS,
|
||||||
|
VERSION_2024_04_15,
|
||||||
|
API_VERSIONS_ENUM,
|
||||||
|
CAL_API_VERSION_HEADER,
|
||||||
|
X_CAL_CLIENT_ID,
|
||||||
|
X_CAL_SECRET_KEY,
|
||||||
|
} from "@calcom/platform-constants";
|
||||||
|
|
||||||
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
||||||
|
|
||||||
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
|
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
|
|
||||||
app.enableVersioning({
|
app.enableVersioning({
|
||||||
type: VersioningType.URI,
|
type: VersioningType.CUSTOM,
|
||||||
prefix: "v",
|
extractor: (request: unknown) => {
|
||||||
defaultVersion: "1",
|
const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined;
|
||||||
|
console.log("asap header headerVersion", headerVersion);
|
||||||
|
if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) {
|
||||||
|
console.log("asap return header headerVersion", headerVersion);
|
||||||
|
return headerVersion;
|
||||||
|
}
|
||||||
|
return VERSION_2024_04_15;
|
||||||
|
},
|
||||||
|
defaultVersion: VERSION_2024_04_15,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
@@ -28,7 +45,15 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
|||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: "*",
|
origin: "*",
|
||||||
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
|
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
|
||||||
allowedHeaders: [X_CAL_CLIENT_ID, X_CAL_SECRET_KEY, "Accept", "Authorization", "Content-Type", "Origin"],
|
allowedHeaders: [
|
||||||
|
X_CAL_CLIENT_ID,
|
||||||
|
X_CAL_SECRET_KEY,
|
||||||
|
CAL_API_VERSION_HEADER,
|
||||||
|
"Accept",
|
||||||
|
"Authorization",
|
||||||
|
"Content-Type",
|
||||||
|
"Origin",
|
||||||
|
],
|
||||||
maxAge: 86_400,
|
maxAge: 86_400,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,15 +71,11 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (process.env.SENTRY_DSN) {
|
|
||||||
Sentry.init({
|
|
||||||
dsn: getEnv("SENTRY_DSN"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exception filters, new filters go at the bottom, keep the order
|
// Exception filters, new filters go at the bottom, keep the order
|
||||||
const { httpAdapter } = app.get(HttpAdapterHost);
|
const { httpAdapter } = app.get(HttpAdapterHost);
|
||||||
app.useGlobalFilters(new SentryFilter(httpAdapter));
|
if (process.env.SENTRY_DSN) {
|
||||||
|
Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter));
|
||||||
|
}
|
||||||
app.useGlobalFilters(new PrismaExceptionFilter());
|
app.useGlobalFilters(new PrismaExceptionFilter());
|
||||||
app.useGlobalFilters(new ZodExceptionFilter());
|
app.useGlobalFilters(new ZodExceptionFilter());
|
||||||
app.useGlobalFilters(new HttpExceptionFilter());
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { bootstrap } from "@/app";
|
|||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
||||||
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
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 { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
import { INestApplication } from "@nestjs/common";
|
import { INestApplication } from "@nestjs/common";
|
||||||
@@ -20,7 +19,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
|||||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
import { handleNewBooking } from "@calcom/platform-libraries";
|
import { handleNewBooking } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
describe("Bookings Endpoints", () => {
|
describe("Bookings Endpoints", () => {
|
||||||
@@ -29,7 +28,7 @@ describe("Bookings Endpoints", () => {
|
|||||||
|
|
||||||
let userRepositoryFixture: UserRepositoryFixture;
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||||
let schedulesService: SchedulesService;
|
let schedulesService: SchedulesService_2024_04_15;
|
||||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
|
|
||||||
const userEmail = "bookings-controller-e2e@api.com";
|
const userEmail = "bookings-controller-e2e@api.com";
|
||||||
@@ -43,7 +42,7 @@ describe("Bookings Endpoints", () => {
|
|||||||
const moduleRef = await withAccessTokenAuth(
|
const moduleRef = await withAccessTokenAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule, SchedulesModule],
|
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.overrideGuard(PermissionsGuard)
|
.overrideGuard(PermissionsGuard)
|
||||||
@@ -55,13 +54,13 @@ describe("Bookings Endpoints", () => {
|
|||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
schedulesService = moduleRef.get<SchedulesService>(SchedulesService);
|
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||||
|
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
});
|
});
|
||||||
|
|
||||||
const userSchedule: CreateScheduleInput = {
|
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||||
name: "working time",
|
name: "working time",
|
||||||
timeZone: "Europe/Rome",
|
timeZone: "Europe/Rome",
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
|||||||
import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input";
|
import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input";
|
||||||
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
||||||
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
@@ -37,14 +38,14 @@ import {
|
|||||||
getBookingInfo,
|
getBookingInfo,
|
||||||
handleCancelBooking,
|
handleCancelBooking,
|
||||||
getBookingForReschedule,
|
getBookingForReschedule,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries-0.0.2";
|
||||||
import {
|
import {
|
||||||
handleNewBooking,
|
handleNewBooking,
|
||||||
BookingResponse,
|
BookingResponse,
|
||||||
HttpError,
|
HttpError,
|
||||||
handleNewRecurringBooking,
|
handleNewRecurringBooking,
|
||||||
handleInstantMeeting,
|
handleInstantMeeting,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries-0.0.2";
|
||||||
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
|
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
|
||||||
import { ApiResponse } from "@calcom/platform-types";
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
@@ -72,8 +73,8 @@ const DEFAULT_PLATFORM_PARAMS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/bookings",
|
path: "/v2/bookings",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@UseGuards(PermissionsGuard)
|
@UseGuards(PermissionsGuard)
|
||||||
@DocsTags("Bookings")
|
@DocsTags("Bookings")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||||
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
||||||
|
|
||||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
import type { AppsStatus } from "@calcom/platform-libraries-0.0.2";
|
||||||
|
|
||||||
export class CreateRecurringBookingInput extends CreateBookingInput {
|
export class CreateRecurringBookingInput extends CreateBookingInput {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.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 { PlatformOAuthClient, Team, User, Credential } from "@prisma/client";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.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 {
|
||||||
|
GOOGLE_CALENDAR,
|
||||||
|
OFFICE_365_CALENDAR,
|
||||||
|
GOOGLE_CALENDAR_TYPE,
|
||||||
|
GOOGLE_CALENDAR_ID,
|
||||||
|
} from "@calcom/platform-constants";
|
||||||
|
import { OFFICE_365_CALENDAR_ID, OFFICE_365_CALENDAR_TYPE } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
const CLIENT_REDIRECT_URI = "http://localhost:5555";
|
||||||
|
|
||||||
|
class CalendarsServiceMock {
|
||||||
|
async getCalendars() {
|
||||||
|
return {
|
||||||
|
connectedCalendars: [
|
||||||
|
{
|
||||||
|
integration: {
|
||||||
|
type: "google_calendar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Platform Calendars Endpoints", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let oAuthClient: PlatformOAuthClient;
|
||||||
|
let organization: Team;
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||||
|
let credentialsRepositoryFixture: CredentialsRepositoryFixture;
|
||||||
|
let user: User;
|
||||||
|
let office365Credentials: Credential;
|
||||||
|
let googleCalendarCredentials: Credential;
|
||||||
|
let accessTokenSecret: string;
|
||||||
|
let refreshTokenSecret: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await Test.createTestingModule({
|
||||||
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
|
imports: [AppModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
.overrideGuard(PermissionsGuard)
|
||||||
|
.useValue({
|
||||||
|
canActivate: () => true,
|
||||||
|
})
|
||||||
|
.overrideProvider(CalendarsService)
|
||||||
|
.useClass(CalendarsServiceMock)
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
|
||||||
|
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);
|
||||||
|
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||||
|
oAuthClient = await createOAuthClient(organization.id);
|
||||||
|
user = await userRepositoryFixture.createOAuthManagedUser("office365-connect@gmail.com", oAuthClient.id);
|
||||||
|
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClient.id);
|
||||||
|
accessTokenSecret = tokens.accessToken;
|
||||||
|
refreshTokenSecret = tokens.refreshToken;
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createOAuthClient(organizationId: number) {
|
||||||
|
const data = {
|
||||||
|
logo: "logo-url",
|
||||||
|
name: "name",
|
||||||
|
redirectUris: [CLIENT_REDIRECT_URI],
|
||||||
|
permissions: 32,
|
||||||
|
};
|
||||||
|
const secret = "secret";
|
||||||
|
|
||||||
|
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(oauthClientRepositoryFixture).toBeDefined();
|
||||||
|
expect(userRepositoryFixture).toBeDefined();
|
||||||
|
expect(oAuthClient).toBeDefined();
|
||||||
|
expect(accessTokenSecret).toBeDefined();
|
||||||
|
expect(refreshTokenSecret).toBeDefined();
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/connect: it should respond 401 with invalid access token`, async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/${OFFICE_365_CALENDAR}/connect`)
|
||||||
|
.set("Authorization", `Bearer invalid_access_token`)
|
||||||
|
.expect(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/connect: it should redirect to auth-url for office 365 calendar oauth with valid access token `, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/${OFFICE_365_CALENDAR}/connect`)
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
|
.expect(200);
|
||||||
|
const data = response.body.data;
|
||||||
|
expect(data.authUrl).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${GOOGLE_CALENDAR}/connect: it should redirect to auth-url for google calendar oauth with valid access token `, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/${GOOGLE_CALENDAR}/connect`)
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
|
.expect(200);
|
||||||
|
const data = response.body.data;
|
||||||
|
expect(data.authUrl).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/random-calendar/connect: it should respond 400 with a message saying the calendar type is invalid`, async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/random-calendar/connect`)
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/save: without access token`, async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(
|
||||||
|
`/v2/calendars/${OFFICE_365_CALENDAR}/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=User.Read%20Calendars.Read%20Calendars.ReadWrite%20offline_access`
|
||||||
|
)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/save: without origin`, async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(
|
||||||
|
`/v2/calendars/${OFFICE_365_CALENDAR}/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=User.Read%20Calendars.Read%20Calendars.ReadWrite%20offline_access`
|
||||||
|
)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/check without access token`, async () => {
|
||||||
|
await request(app.getHttpServer()).get(`/v2/calendars/${OFFICE_365_CALENDAR}/check`).expect(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/check with no credentials`, async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/${OFFICE_365_CALENDAR}/check`)
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/check with access token, origin and office365 credentials`, async () => {
|
||||||
|
office365Credentials = await credentialsRepositoryFixture.create(
|
||||||
|
OFFICE_365_CALENDAR_TYPE,
|
||||||
|
{},
|
||||||
|
user.id,
|
||||||
|
OFFICE_365_CALENDAR_ID
|
||||||
|
);
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/${OFFICE_365_CALENDAR}/check`)
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${GOOGLE_CALENDAR}/check with access token, origin and google calendar credentials`, async () => {
|
||||||
|
googleCalendarCredentials = await credentialsRepositoryFixture.create(
|
||||||
|
GOOGLE_CALENDAR_TYPE,
|
||||||
|
{},
|
||||||
|
user.id,
|
||||||
|
GOOGLE_CALENDAR_ID
|
||||||
|
);
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/${GOOGLE_CALENDAR}/check`)
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
await credentialsRepositoryFixture.delete(office365Credentials.id);
|
||||||
|
await credentialsRepositoryFixture.delete(googleCalendarCredentials.id);
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
export interface CalendarApp {
|
||||||
|
save(state: string, code: string, origin: string): Promise<{ url: string }>;
|
||||||
|
check(userId: number): Promise<ApiResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OAuthCalendarApp extends CalendarApp {
|
||||||
|
connect(authorization: string, req: Request): Promise<ApiResponse<{ authUrl: string }>>;
|
||||||
|
}
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
|
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
|
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
|
||||||
|
import { OutlookService } from "@/ee/calendars/services/outlook.service";
|
||||||
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, UsersModule],
|
imports: [PrismaModule, UsersModule, TokensModule],
|
||||||
providers: [CredentialsRepository, CalendarsService],
|
providers: [
|
||||||
|
CredentialsRepository,
|
||||||
|
CalendarsService,
|
||||||
|
OutlookService,
|
||||||
|
GoogleCalendarService,
|
||||||
|
SelectedCalendarsRepository,
|
||||||
|
AppsRepository,
|
||||||
|
],
|
||||||
controllers: [CalendarsController],
|
controllers: [CalendarsController],
|
||||||
exports: [CalendarsService],
|
exports: [CalendarsService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,24 +1,48 @@
|
|||||||
import { GetBusyTimesOutput } from "@/ee/calendars/outputs/busy-times.output";
|
import { GetBusyTimesOutput } from "@/ee/calendars/outputs/busy-times.output";
|
||||||
import { ConnectedCalendarsOutput } from "@/ee/calendars/outputs/connected-calendars.output";
|
import { ConnectedCalendarsOutput } from "@/ee/calendars/outputs/connected-calendars.output";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
|
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
|
||||||
|
import { OutlookService } from "@/ee/calendars/services/outlook.service";
|
||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Controller, Get, UseGuards, Query } from "@nestjs/common";
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
UseGuards,
|
||||||
|
Query,
|
||||||
|
HttpStatus,
|
||||||
|
HttpCode,
|
||||||
|
Req,
|
||||||
|
Param,
|
||||||
|
Headers,
|
||||||
|
Redirect,
|
||||||
|
BadRequestException,
|
||||||
|
} from "@nestjs/common";
|
||||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
import { Request } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { APPS_READ } from "@calcom/platform-constants";
|
||||||
import { CalendarBusyTimesInput } from "@calcom/platform-types";
|
import { SUCCESS_STATUS, CALENDARS, GOOGLE_CALENDAR, OFFICE_365_CALENDAR } from "@calcom/platform-constants";
|
||||||
|
import { ApiResponse, CalendarBusyTimesInput } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/calendars",
|
path: "/v2/calendars",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard)
|
|
||||||
@DocsTags("Calendars")
|
@DocsTags("Calendars")
|
||||||
export class CalendarsController {
|
export class CalendarsController {
|
||||||
constructor(private readonly calendarsService: CalendarsService) {}
|
constructor(
|
||||||
|
private readonly calendarsService: CalendarsService,
|
||||||
|
private readonly outlookService: OutlookService,
|
||||||
|
private readonly googleCalendarService: GoogleCalendarService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
@Get("/busy-times")
|
@Get("/busy-times")
|
||||||
async getBusyTimes(
|
async getBusyTimes(
|
||||||
@Query() queryParams: CalendarBusyTimesInput,
|
@Query() queryParams: CalendarBusyTimesInput,
|
||||||
@@ -47,6 +71,7 @@ export class CalendarsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
|
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
|
||||||
const calendars = await this.calendarsService.getCalendars(userId);
|
const calendars = await this.calendarsService.getCalendars(userId);
|
||||||
|
|
||||||
@@ -55,4 +80,74 @@ export class CalendarsController {
|
|||||||
data: calendars,
|
data: calendars,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
@Get("/:calendar/connect")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async redirect(
|
||||||
|
@Req() req: Request,
|
||||||
|
@Headers("Authorization") authorization: string,
|
||||||
|
@Param("calendar") calendar: string,
|
||||||
|
@Query("redir") redir?: string | null
|
||||||
|
): Promise<ApiResponse<{ authUrl: string }>> {
|
||||||
|
switch (calendar) {
|
||||||
|
case OFFICE_365_CALENDAR:
|
||||||
|
return await this.outlookService.connect(authorization, req, redir ?? "");
|
||||||
|
case GOOGLE_CALENDAR:
|
||||||
|
return await this.googleCalendarService.connect(authorization, req, redir ?? "");
|
||||||
|
default:
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Invalid calendar type, available calendars are: ",
|
||||||
|
CALENDARS.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/:calendar/save")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Redirect(undefined, 301)
|
||||||
|
async save(
|
||||||
|
@Query("state") state: string,
|
||||||
|
@Query("code") code: string,
|
||||||
|
@Param("calendar") calendar: string
|
||||||
|
): Promise<{ url: string }> {
|
||||||
|
// state params contains our user access token
|
||||||
|
const stateParams = new URLSearchParams(state);
|
||||||
|
const { accessToken, origin, redir } = z
|
||||||
|
.object({ accessToken: z.string(), origin: z.string(), redir: z.string().nullish().optional() })
|
||||||
|
.parse({
|
||||||
|
accessToken: stateParams.get("accessToken"),
|
||||||
|
origin: stateParams.get("origin"),
|
||||||
|
redir: stateParams.get("redir"),
|
||||||
|
});
|
||||||
|
switch (calendar) {
|
||||||
|
case OFFICE_365_CALENDAR:
|
||||||
|
return await this.outlookService.save(code, accessToken, origin, redir ?? "");
|
||||||
|
case GOOGLE_CALENDAR:
|
||||||
|
return await this.googleCalendarService.save(code, accessToken, origin, redir ?? "");
|
||||||
|
default:
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Invalid calendar type, available calendars are: ",
|
||||||
|
CALENDARS.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/:calendar/check")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||||
|
@Permissions([APPS_READ])
|
||||||
|
async check(@GetUser("id") userId: number, @Param("calendar") calendar: string): Promise<ApiResponse> {
|
||||||
|
switch (calendar) {
|
||||||
|
case OFFICE_365_CALENDAR:
|
||||||
|
return await this.outlookService.check(userId);
|
||||||
|
case GOOGLE_CALENDAR:
|
||||||
|
return await this.googleCalendarService.check(userId);
|
||||||
|
default:
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Invalid calendar type, available calendars are: ",
|
||||||
|
CALENDARS.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import {
|
import {
|
||||||
CredentialsRepository,
|
CredentialsRepository,
|
||||||
CredentialsWithUserEmail,
|
CredentialsWithUserEmail,
|
||||||
@@ -11,21 +12,27 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getConnectedDestinationCalendars } from "@calcom/platform-libraries";
|
import { getConnectedDestinationCalendars } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { getBusyCalendarTimes } from "@calcom/platform-libraries";
|
import { getBusyCalendarTimes } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { Calendar } from "@calcom/platform-types";
|
import { Calendar } from "@calcom/platform-types";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CalendarsService {
|
export class CalendarsService {
|
||||||
|
private oAuthCalendarResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
private readonly credentialsRepository: CredentialsRepository,
|
private readonly credentialsRepository: CredentialsRepository,
|
||||||
|
private readonly appsRepository: AppsRepository,
|
||||||
private readonly dbRead: PrismaReadService,
|
private readonly dbRead: PrismaReadService,
|
||||||
private readonly dbWrite: PrismaWriteService
|
private readonly dbWrite: PrismaWriteService,
|
||||||
|
private readonly config: ConfigService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getCalendars(userId: number) {
|
async getCalendars(userId: number) {
|
||||||
@@ -110,4 +117,24 @@ export class CalendarsService {
|
|||||||
});
|
});
|
||||||
return composedSelectedCalendars;
|
return composedSelectedCalendars;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAppKeys(appName: string) {
|
||||||
|
const app = await this.appsRepository.getAppBySlug(appName);
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { client_id, client_secret } = this.oAuthCalendarResponseSchema.parse(app.keys);
|
||||||
|
|
||||||
|
if (!client_id) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client_secret) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { client_id, client_secret };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
|
||||||
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
|
import { Logger, NotFoundException } from "@nestjs/common";
|
||||||
|
import { BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { Request } from "express";
|
||||||
|
import { google } from "googleapis";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, GOOGLE_CALENDAR_TYPE } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
const CALENDAR_SCOPES = [
|
||||||
|
"https://www.googleapis.com/auth/calendar.readonly",
|
||||||
|
"https://www.googleapis.com/auth/calendar.events",
|
||||||
|
];
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GoogleCalendarService implements OAuthCalendarApp {
|
||||||
|
private redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
|
||||||
|
private gcalResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
|
||||||
|
private logger = new Logger("GcalService");
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly appsRepository: AppsRepository,
|
||||||
|
private readonly credentialRepository: CredentialsRepository,
|
||||||
|
private readonly calendarsService: CalendarsService,
|
||||||
|
private readonly tokensRepository: TokensRepository,
|
||||||
|
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async connect(
|
||||||
|
authorization: string,
|
||||||
|
req: Request,
|
||||||
|
redir?: string
|
||||||
|
): Promise<{ status: typeof SUCCESS_STATUS; data: { authUrl: string } }> {
|
||||||
|
const accessToken = authorization.replace("Bearer ", "");
|
||||||
|
const origin = req.get("origin") ?? req.get("host");
|
||||||
|
const redirectUrl = await await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
|
||||||
|
|
||||||
|
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(code: string, accessToken: string, origin: string, redir?: string): Promise<{ url: string }> {
|
||||||
|
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir);
|
||||||
|
}
|
||||||
|
|
||||||
|
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
|
||||||
|
return await this.checkIfCalendarConnected(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string) {
|
||||||
|
const oAuth2Client = await this.getOAuthClient(this.redirectUri);
|
||||||
|
|
||||||
|
const authUrl = oAuth2Client.generateAuthUrl({
|
||||||
|
access_type: "offline",
|
||||||
|
scope: CALENDAR_SCOPES,
|
||||||
|
prompt: "consent",
|
||||||
|
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return authUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOAuthClient(redirectUri: string) {
|
||||||
|
this.logger.log("Getting Google Calendar OAuth Client");
|
||||||
|
const app = await this.appsRepository.getAppBySlug("google-calendar");
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);
|
||||||
|
|
||||||
|
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
||||||
|
return oAuth2Client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkIfCalendarConnected(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
|
||||||
|
const gcalCredentials = await this.credentialRepository.getByTypeAndUserId("google_calendar", userId);
|
||||||
|
|
||||||
|
if (!gcalCredentials) {
|
||||||
|
throw new BadRequestException("Credentials for google_calendar not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gcalCredentials.invalid) {
|
||||||
|
throw new BadRequestException("Invalid google oauth credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
|
||||||
|
const googleCalendar = connectedCalendars.find(
|
||||||
|
(cal: { integration: { type: string } }) => cal.integration.type === GOOGLE_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
if (!googleCalendar) {
|
||||||
|
throw new UnauthorizedException("Google Calendar not connected.");
|
||||||
|
}
|
||||||
|
if (googleCalendar.error?.message) {
|
||||||
|
throw new UnauthorizedException(googleCalendar.error?.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: SUCCESS_STATUS };
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveCalendarCredentialsAndRedirect(
|
||||||
|
code: string,
|
||||||
|
accessToken: string,
|
||||||
|
origin: string,
|
||||||
|
redir?: string
|
||||||
|
) {
|
||||||
|
// User chose not to authorize your app or didn't authorize your app
|
||||||
|
// redirect directly without oauth code
|
||||||
|
if (!code) {
|
||||||
|
return { url: redir || origin };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedCode = z.string().parse(code);
|
||||||
|
|
||||||
|
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
||||||
|
|
||||||
|
if (!ownerId) {
|
||||||
|
throw new UnauthorizedException("Invalid Access token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const oAuth2Client = await this.getOAuthClient(this.redirectUri);
|
||||||
|
const token = await oAuth2Client.getToken(parsedCode);
|
||||||
|
// Google oAuth Credentials are stored in token.tokens
|
||||||
|
const key = token.tokens;
|
||||||
|
const credential = await this.credentialRepository.createAppCredential(
|
||||||
|
GOOGLE_CALENDAR_TYPE,
|
||||||
|
key as Prisma.InputJsonValue,
|
||||||
|
ownerId
|
||||||
|
);
|
||||||
|
|
||||||
|
oAuth2Client.setCredentials(key);
|
||||||
|
|
||||||
|
const calendar = google.calendar({
|
||||||
|
version: "v3",
|
||||||
|
auth: oAuth2Client,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cals = await calendar.calendarList.list({ fields: "items(id,summary,primary,accessRole)" });
|
||||||
|
|
||||||
|
const primaryCal = cals.data.items?.find((cal) => cal.primary);
|
||||||
|
|
||||||
|
if (primaryCal?.id) {
|
||||||
|
await this.selectedCalendarsRepository.createSelectedCalendar(
|
||||||
|
primaryCal.id,
|
||||||
|
credential.id,
|
||||||
|
ownerId,
|
||||||
|
GOOGLE_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { url: redir || origin };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
|
||||||
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
|
import type { Calendar as OfficeCalendar } from "@microsoft/microsoft-graph-types-beta";
|
||||||
|
import { BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { Request } from "express";
|
||||||
|
import { stringify } from "querystring";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SUCCESS_STATUS,
|
||||||
|
OFFICE_365_CALENDAR,
|
||||||
|
OFFICE_365_CALENDAR_ID,
|
||||||
|
OFFICE_365_CALENDAR_TYPE,
|
||||||
|
} from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OutlookService implements OAuthCalendarApp {
|
||||||
|
private redirectUri = `${this.config.get("api.url")}/calendars/${OFFICE_365_CALENDAR}/save`;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly calendarsService: CalendarsService,
|
||||||
|
private readonly credentialRepository: CredentialsRepository,
|
||||||
|
private readonly tokensRepository: TokensRepository,
|
||||||
|
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async connect(
|
||||||
|
authorization: string,
|
||||||
|
req: Request,
|
||||||
|
redir?: string
|
||||||
|
): Promise<{ status: typeof SUCCESS_STATUS; data: { authUrl: string } }> {
|
||||||
|
const accessToken = authorization.replace("Bearer ", "");
|
||||||
|
const origin = req.get("origin") ?? req.get("host");
|
||||||
|
const redirectUrl = await await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
|
||||||
|
|
||||||
|
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(code: string, accessToken: string, origin: string, redir?: string): Promise<{ url: string }> {
|
||||||
|
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir);
|
||||||
|
}
|
||||||
|
|
||||||
|
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
|
||||||
|
return await this.checkIfCalendarConnected(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string) {
|
||||||
|
const { client_id } = await this.calendarsService.getAppKeys(OFFICE_365_CALENDAR_ID);
|
||||||
|
|
||||||
|
const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
|
||||||
|
const params = {
|
||||||
|
response_type: "code",
|
||||||
|
scope: scopes.join(" "),
|
||||||
|
client_id,
|
||||||
|
prompt: "select_account",
|
||||||
|
redirect_uri: this.redirectUri,
|
||||||
|
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = stringify(params);
|
||||||
|
|
||||||
|
const url = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${query}`;
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkIfCalendarConnected(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
|
||||||
|
const office365CalendarCredentials = await this.credentialRepository.getByTypeAndUserId(
|
||||||
|
"office365_calendar",
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!office365CalendarCredentials) {
|
||||||
|
throw new BadRequestException("Credentials for office_365_calendar not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (office365CalendarCredentials.invalid) {
|
||||||
|
throw new BadRequestException("Invalid office 365 calendar credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
|
||||||
|
const office365Calendar = connectedCalendars.find(
|
||||||
|
(cal: { integration: { type: string } }) => cal.integration.type === OFFICE_365_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
if (!office365Calendar) {
|
||||||
|
throw new UnauthorizedException("Office 365 calendar not connected.");
|
||||||
|
}
|
||||||
|
if (office365Calendar.error?.message) {
|
||||||
|
throw new UnauthorizedException(office365Calendar.error?.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOAuthCredentials(code: string) {
|
||||||
|
const scopes = ["offline_access", "Calendars.Read", "Calendars.ReadWrite"];
|
||||||
|
const { client_id, client_secret } = await this.calendarsService.getAppKeys(OFFICE_365_CALENDAR_ID);
|
||||||
|
|
||||||
|
const toUrlEncoded = (payload: Record<string, string>) =>
|
||||||
|
Object.keys(payload)
|
||||||
|
.map((key) => `${key}=${encodeURIComponent(payload[key])}`)
|
||||||
|
.join("&");
|
||||||
|
|
||||||
|
const body = toUrlEncoded({
|
||||||
|
client_id,
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code,
|
||||||
|
scope: scopes.join(" "),
|
||||||
|
redirect_uri: this.redirectUri,
|
||||||
|
client_secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseBody = await response.json();
|
||||||
|
|
||||||
|
return responseBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDefaultCalendar(accessToken: string): Promise<OfficeCalendar> {
|
||||||
|
const response = await fetch("https://graph.microsoft.com/v1.0/me/calendar", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseBody = await response.json();
|
||||||
|
|
||||||
|
return responseBody as OfficeCalendar;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveCalendarCredentialsAndRedirect(
|
||||||
|
code: string,
|
||||||
|
accessToken: string,
|
||||||
|
origin: string,
|
||||||
|
redir?: string
|
||||||
|
) {
|
||||||
|
// if code is not defined, user denied to authorize office 365 app, just redirect straight away
|
||||||
|
if (!code) {
|
||||||
|
return { url: redir || origin };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedCode = z.string().parse(code);
|
||||||
|
|
||||||
|
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
||||||
|
|
||||||
|
if (!ownerId) {
|
||||||
|
throw new UnauthorizedException("Invalid Access token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const office365OAuthCredentials = await this.getOAuthCredentials(parsedCode);
|
||||||
|
|
||||||
|
const defaultCalendar = await this.getDefaultCalendar(office365OAuthCredentials.access_token);
|
||||||
|
|
||||||
|
if (defaultCalendar?.id) {
|
||||||
|
const credential = await this.credentialRepository.createAppCredential(
|
||||||
|
OFFICE_365_CALENDAR_TYPE,
|
||||||
|
office365OAuthCredentials,
|
||||||
|
ownerId
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.selectedCalendarsRepository.createSelectedCalendar(
|
||||||
|
defaultCalendar.id,
|
||||||
|
credential.id,
|
||||||
|
ownerId,
|
||||||
|
OFFICE_365_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: redir || origin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
EventTypesPublic,
|
EventTypesPublic,
|
||||||
eventTypeBookingFields,
|
eventTypeBookingFields,
|
||||||
eventTypeLocations,
|
eventTypeLocations,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries-0.0.2";
|
||||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
describe("Event types Endpoints", () => {
|
describe("Event types Endpoints", () => {
|
||||||
@@ -204,6 +204,16 @@ describe("Event types Endpoints", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return 400 if param event type id is null", async () => {
|
||||||
|
const locations = [{ type: "inPerson", address: "123 Main St" }];
|
||||||
|
|
||||||
|
const body: UpdateEventTypeInput = {
|
||||||
|
locations,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer()).patch(`/api/v2/event-types/null`).send(body).expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
it("should update event type locations", async () => {
|
it("should update event type locations", async () => {
|
||||||
const locations = [{ type: "inPerson", address: "123 Main St" }];
|
const locations = [{ type: "inPerson", address: "123 Main St" }];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||||
|
import { EventTypeIdParams } from "@/ee/event-types/inputs/event-type-id.input";
|
||||||
import { GetPublicEventTypeQueryParams } from "@/ee/event-types/inputs/get-public-event-type-query-params.input";
|
import { GetPublicEventTypeQueryParams } from "@/ee/event-types/inputs/get-public-event-type-query-params.input";
|
||||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||||
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
|
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
|
||||||
@@ -9,6 +10,7 @@ import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-ty
|
|||||||
import { GetEventTypesData, GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
|
import { GetEventTypesData, GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
|
||||||
import { UpdateEventTypeOutput } from "@/ee/event-types/outputs/update-event-type.output";
|
import { UpdateEventTypeOutput } from "@/ee/event-types/outputs/update-event-type.output";
|
||||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
@@ -29,17 +31,18 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Query,
|
Query,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
|
ParseIntPipe,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { getPublicEvent } from "@calcom/platform-libraries";
|
import { getPublicEvent } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { getEventTypesByViewer } from "@calcom/platform-libraries";
|
import { getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "event-types",
|
path: "/v2/event-types",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@UseGuards(PermissionsGuard)
|
@UseGuards(PermissionsGuard)
|
||||||
@DocsTags("Event types")
|
@DocsTags("Event types")
|
||||||
@@ -68,7 +71,9 @@ export class EventTypesController {
|
|||||||
@Permissions([EVENT_TYPE_READ])
|
@Permissions([EVENT_TYPE_READ])
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(AccessTokenGuard)
|
||||||
async getEventType(
|
async getEventType(
|
||||||
@Param("eventTypeId") eventTypeId: string,
|
@Param() params: EventTypeIdParams,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
|
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
): Promise<GetEventTypeOutput> {
|
): Promise<GetEventTypeOutput> {
|
||||||
const eventType = await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId));
|
const eventType = await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId));
|
||||||
@@ -144,7 +149,8 @@ export class EventTypesController {
|
|||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(AccessTokenGuard)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async updateEventType(
|
async updateEventType(
|
||||||
@Param("eventTypeId") eventTypeId: number,
|
@Param() params: EventTypeIdParams,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
@Body() body: UpdateEventTypeInput,
|
@Body() body: UpdateEventTypeInput,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
): Promise<UpdateEventTypeOutput> {
|
): Promise<UpdateEventTypeOutput> {
|
||||||
@@ -160,7 +166,8 @@ export class EventTypesController {
|
|||||||
@Permissions([EVENT_TYPE_WRITE])
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(AccessTokenGuard)
|
||||||
async deleteEventType(
|
async deleteEventType(
|
||||||
@Param("eventTypeId") eventTypeId: number,
|
@Param() params: EventTypeIdParams,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
@GetUser("id") userId: number
|
@GetUser("id") userId: number
|
||||||
): Promise<DeleteEventTypeOutput> {
|
): Promise<DeleteEventTypeOutput> {
|
||||||
const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId);
|
const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
|||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import { getEventTypeById } from "@calcom/platform-libraries";
|
import { getEventTypeById } from "@calcom/platform-libraries-0.0.2";
|
||||||
import type { PrismaClient } from "@calcom/prisma";
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsNumberString } from "class-validator";
|
||||||
|
|
||||||
|
export class EventTypeIdParams {
|
||||||
|
@IsNumberString()
|
||||||
|
eventTypeId!: number;
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@ import { SelectedCalendarsRepository } from "@/modules/selected-calendars/select
|
|||||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
|
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { EventType } from "@calcom/prisma/client";
|
import { EventType } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
|||||||
import { GcalAuthUrlOutput } from "@/ee/gcal/outputs/auth-url.output";
|
import { GcalAuthUrlOutput } from "@/ee/gcal/outputs/auth-url.output";
|
||||||
import { GcalCheckOutput } from "@/ee/gcal/outputs/check.output";
|
import { GcalCheckOutput } from "@/ee/gcal/outputs/check.output";
|
||||||
import { GcalSaveRedirectOutput } from "@/ee/gcal/outputs/save-redirect.output";
|
import { GcalSaveRedirectOutput } from "@/ee/gcal/outputs/save-redirect.output";
|
||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { GCalService } from "@/modules/apps/services/gcal.service";
|
import { GCalService } from "@/modules/apps/services/gcal.service";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
@@ -40,8 +41,8 @@ const CALENDAR_SCOPES = [
|
|||||||
|
|
||||||
// Controller for the GCalConnect Atom
|
// Controller for the GCalConnect Atom
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/gcal",
|
path: "/v2/gcal",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@DocsTags("Google Calendar")
|
@DocsTags("Google Calendar")
|
||||||
export class GcalController {
|
export class GcalController {
|
||||||
@@ -81,56 +82,10 @@ export class GcalController {
|
|||||||
@Redirect(undefined, 301)
|
@Redirect(undefined, 301)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async save(@Query("state") state: string, @Query("code") code: string): Promise<GcalSaveRedirectOutput> {
|
async save(@Query("state") state: string, @Query("code") code: string): Promise<GcalSaveRedirectOutput> {
|
||||||
const stateParams = new URLSearchParams(state);
|
const url = new URL(this.config.get("api.url") + "/calendars/google/save");
|
||||||
const { accessToken, origin } = z
|
url.searchParams.append("code", code);
|
||||||
.object({ accessToken: z.string(), origin: z.string() })
|
url.searchParams.append("state", state);
|
||||||
.parse({ accessToken: stateParams.get("accessToken"), origin: stateParams.get("origin") });
|
return { url: url.href };
|
||||||
|
|
||||||
// User chose not to authorize your app or didn't authorize your app
|
|
||||||
// redirect directly without oauth code
|
|
||||||
if (!code) {
|
|
||||||
return { url: origin };
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedCode = z.string().parse(code);
|
|
||||||
|
|
||||||
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
|
||||||
|
|
||||||
if (!ownerId) {
|
|
||||||
throw new UnauthorizedException("Invalid Access token.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const oAuth2Client = await this.gcalService.getOAuthClient(this.redirectUri);
|
|
||||||
const token = await oAuth2Client.getToken(parsedCode);
|
|
||||||
// Google oAuth Credentials are stored in token.tokens
|
|
||||||
const key = token.tokens;
|
|
||||||
const credential = await this.credentialRepository.createAppCredential(
|
|
||||||
GOOGLE_CALENDAR_TYPE,
|
|
||||||
key as Prisma.InputJsonValue,
|
|
||||||
ownerId
|
|
||||||
);
|
|
||||||
|
|
||||||
oAuth2Client.setCredentials(key);
|
|
||||||
|
|
||||||
const calendar = google.calendar({
|
|
||||||
version: "v3",
|
|
||||||
auth: oAuth2Client,
|
|
||||||
});
|
|
||||||
|
|
||||||
const cals = await calendar.calendarList.list({ fields: "items(id,summary,primary,accessRole)" });
|
|
||||||
|
|
||||||
const primaryCal = cals.data.items?.find((cal) => cal.primary);
|
|
||||||
|
|
||||||
if (primaryCal?.id) {
|
|
||||||
await this.selectedCalendarsRepository.createSelectedCalendar(
|
|
||||||
primaryCal.id,
|
|
||||||
credential.id,
|
|
||||||
ownerId,
|
|
||||||
GOOGLE_CALENDAR_TYPE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { url: origin };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("/check")
|
@Get("/check")
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||||
@@ -34,14 +33,7 @@ describe("Me Endpoints", () => {
|
|||||||
const moduleRef = await withAccessTokenAuth(
|
const moduleRef = await withAccessTokenAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
imports: [
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_04_15],
|
||||||
AppModule,
|
|
||||||
PrismaModule,
|
|
||||||
AvailabilitiesModule,
|
|
||||||
UsersModule,
|
|
||||||
TokensModule,
|
|
||||||
SchedulesModule,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.overrideGuard(PermissionsGuard)
|
.overrideGuard(PermissionsGuard)
|
||||||
@@ -118,13 +110,13 @@ describe("Me Endpoints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not update user associated with access token given invalid time format", async () => {
|
it("should not update user associated with access token given invalid time format", async () => {
|
||||||
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 };
|
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 as any };
|
||||||
|
|
||||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
|
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not update user associated with access token given invalid week start", async () => {
|
it("should not update user associated with access token given invalid week start", async () => {
|
||||||
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" };
|
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" as any };
|
||||||
|
|
||||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
|
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { GetMeOutput } from "@/ee/me/outputs/get-me.output";
|
import { GetMeOutput } from "@/ee/me/outputs/get-me.output";
|
||||||
import { UpdateMeOutput } from "@/ee/me/outputs/update-me.output";
|
import { UpdateMeOutput } from "@/ee/me/outputs/update-me.output";
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
@@ -14,15 +15,15 @@ import { PROFILE_READ, PROFILE_WRITE, SUCCESS_STATUS } from "@calcom/platform-co
|
|||||||
import { userSchemaResponse } from "@calcom/platform-types";
|
import { userSchemaResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/me",
|
path: "/v2/me",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||||
@DocsTags("Me")
|
@DocsTags("Me")
|
||||||
export class MeController {
|
export class MeController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
private readonly schedulesRepository: SchedulesService
|
private readonly schedulesRepository: SchedulesService_2024_04_15
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { MeController } from "@/ee/me/me.controller";
|
import { MeController } from "@/ee/me/me.controller";
|
||||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UsersModule, SchedulesModule, TokensModule],
|
imports: [UsersModule, SchedulesModule_2024_04_15, TokensModule],
|
||||||
controllers: [MeController],
|
controllers: [MeController],
|
||||||
})
|
})
|
||||||
export class MeModule {}
|
export class MeModule {}
|
||||||
@@ -4,7 +4,8 @@ import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
|||||||
import { GcalModule } from "@/ee/gcal/gcal.module";
|
import { GcalModule } from "@/ee/gcal/gcal.module";
|
||||||
import { MeModule } from "@/ee/me/me.module";
|
import { MeModule } from "@/ee/me/me.module";
|
||||||
import { ProviderModule } from "@/ee/provider/provider.module";
|
import { ProviderModule } from "@/ee/provider/provider.module";
|
||||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
|
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
||||||
import { SlotsModule } from "@/modules/slots/slots.module";
|
import { SlotsModule } from "@/modules/slots/slots.module";
|
||||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
@@ -13,7 +14,8 @@ import { Module } from "@nestjs/common";
|
|||||||
imports: [
|
imports: [
|
||||||
GcalModule,
|
GcalModule,
|
||||||
ProviderModule,
|
ProviderModule,
|
||||||
SchedulesModule,
|
SchedulesModule_2024_04_15,
|
||||||
|
SchedulesModule_2024_06_11,
|
||||||
MeModule,
|
MeModule,
|
||||||
EventTypesModule,
|
EventTypesModule,
|
||||||
CalendarsModule,
|
CalendarsModule,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ProviderVerifyAccessTokenOutput } from "@/ee/provider/outputs/verify-access-token.output";
|
import { ProviderVerifyAccessTokenOutput } from "@/ee/provider/outputs/verify-access-token.output";
|
||||||
import { ProviderVerifyClientOutput } from "@/ee/provider/outputs/verify-client.output";
|
import { ProviderVerifyClientOutput } from "@/ee/provider/outputs/verify-client.output";
|
||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
@@ -20,8 +21,8 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
|
|||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/provider",
|
path: "/v2/provider",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@DocsTags("Cal provider")
|
@DocsTags("Cal provider")
|
||||||
export class CalProviderController {
|
export class CalProviderController {
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { SchedulesController } from "@/ee/schedules/controllers/schedules.controller";
|
|
||||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
|
||||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
|
||||||
import { Module } from "@nestjs/common";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [PrismaModule, AvailabilitiesModule, UsersModule, TokensModule],
|
|
||||||
providers: [SchedulesRepository, SchedulesService],
|
|
||||||
controllers: [SchedulesController],
|
|
||||||
exports: [SchedulesService, SchedulesRepository],
|
|
||||||
})
|
|
||||||
export class SchedulesModule {}
|
|
||||||
+17
-23
@@ -1,12 +1,11 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||||
import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output";
|
import { CreateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/create-schedule.output";
|
||||||
import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output";
|
import { GetSchedulesOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-schedules.output";
|
||||||
import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output";
|
import { UpdateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/update-schedule.output";
|
||||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
@@ -19,8 +18,8 @@ import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.r
|
|||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_04_15 } from "@calcom/platform-constants";
|
||||||
import { UpdateScheduleInput } from "@calcom/platform-types";
|
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
||||||
|
|
||||||
describe("Schedules Endpoints", () => {
|
describe("Schedules Endpoints", () => {
|
||||||
describe("User Authentication", () => {
|
describe("User Authentication", () => {
|
||||||
@@ -32,7 +31,7 @@ describe("Schedules Endpoints", () => {
|
|||||||
const userEmail = "schedules-controller-e2e@api.com";
|
const userEmail = "schedules-controller-e2e@api.com";
|
||||||
let user: User;
|
let user: User;
|
||||||
|
|
||||||
let createdSchedule: CreateScheduleOutput["data"];
|
let createdSchedule: CreateScheduleOutput_2024_04_15["data"];
|
||||||
const defaultAvailabilityDays = [1, 2, 3, 4, 5];
|
const defaultAvailabilityDays = [1, 2, 3, 4, 5];
|
||||||
const defaultAvailabilityStartTime = "1970-01-01T09:00:00.000Z";
|
const defaultAvailabilityStartTime = "1970-01-01T09:00:00.000Z";
|
||||||
const defaultAvailabilityEndTime = "1970-01-01T17:00:00.000Z";
|
const defaultAvailabilityEndTime = "1970-01-01T17:00:00.000Z";
|
||||||
@@ -41,14 +40,7 @@ describe("Schedules Endpoints", () => {
|
|||||||
const moduleRef = await withAccessTokenAuth(
|
const moduleRef = await withAccessTokenAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
imports: [
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_04_15],
|
||||||
AppModule,
|
|
||||||
PrismaModule,
|
|
||||||
AvailabilitiesModule,
|
|
||||||
UsersModule,
|
|
||||||
TokensModule,
|
|
||||||
SchedulesModule,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.overrideGuard(PermissionsGuard)
|
.overrideGuard(PermissionsGuard)
|
||||||
@@ -79,7 +71,7 @@ describe("Schedules Endpoints", () => {
|
|||||||
const scheduleTimeZone = "Europe/Rome";
|
const scheduleTimeZone = "Europe/Rome";
|
||||||
const isDefault = true;
|
const isDefault = true;
|
||||||
|
|
||||||
const body: CreateScheduleInput = {
|
const body: CreateScheduleInput_2024_04_15 = {
|
||||||
name: scheduleName,
|
name: scheduleName,
|
||||||
timeZone: scheduleTimeZone,
|
timeZone: scheduleTimeZone,
|
||||||
isDefault,
|
isDefault,
|
||||||
@@ -87,10 +79,11 @@ describe("Schedules Endpoints", () => {
|
|||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post("/api/v2/schedules")
|
.post("/api/v2/schedules")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseData: CreateScheduleOutput = response.body;
|
const responseData: CreateScheduleOutput_2024_04_15 = response.body;
|
||||||
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseData.data).toBeDefined();
|
expect(responseData.data).toBeDefined();
|
||||||
expect(responseData.data.isDefault).toEqual(isDefault);
|
expect(responseData.data.isDefault).toEqual(isDefault);
|
||||||
@@ -117,7 +110,7 @@ describe("Schedules Endpoints", () => {
|
|||||||
.get("/api/v2/schedules/default")
|
.get("/api/v2/schedules/default")
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseData: CreateScheduleOutput = response.body;
|
const responseData: CreateScheduleOutput_2024_04_15 = response.body;
|
||||||
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseData.data).toBeDefined();
|
expect(responseData.data).toBeDefined();
|
||||||
expect(responseData.data.id).toEqual(createdSchedule.id);
|
expect(responseData.data.id).toEqual(createdSchedule.id);
|
||||||
@@ -137,7 +130,7 @@ describe("Schedules Endpoints", () => {
|
|||||||
.get(`/api/v2/schedules`)
|
.get(`/api/v2/schedules`)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const responseData: GetSchedulesOutput = response.body;
|
const responseData: GetSchedulesOutput_2024_04_15 = response.body;
|
||||||
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseData.data).toBeDefined();
|
expect(responseData.data).toBeDefined();
|
||||||
expect(responseData.data?.[0].id).toEqual(createdSchedule.id);
|
expect(responseData.data?.[0].id).toEqual(createdSchedule.id);
|
||||||
@@ -155,16 +148,17 @@ describe("Schedules Endpoints", () => {
|
|||||||
it("should update schedule name", async () => {
|
it("should update schedule name", async () => {
|
||||||
const newScheduleName = "new-schedule-name";
|
const newScheduleName = "new-schedule-name";
|
||||||
|
|
||||||
const body: UpdateScheduleInput = {
|
const body: UpdateScheduleInput_2024_04_15 = {
|
||||||
name: newScheduleName,
|
name: newScheduleName,
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.patch(`/api/v2/schedules/${createdSchedule.id}`)
|
.patch(`/api/v2/schedules/${createdSchedule.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((response: any) => {
|
.then((response: any) => {
|
||||||
const responseData: UpdateScheduleOutput = response.body;
|
const responseData: UpdateScheduleOutput_2024_04_15 = response.body;
|
||||||
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseData.data).toBeDefined();
|
expect(responseData.data).toBeDefined();
|
||||||
expect(responseData.data.schedule.name).toEqual(newScheduleName);
|
expect(responseData.data.schedule.name).toEqual(newScheduleName);
|
||||||
+29
-22
@@ -1,10 +1,11 @@
|
|||||||
import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output";
|
import { CreateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/create-schedule.output";
|
||||||
import { DeleteScheduleOutput } from "@/ee/schedules/outputs/delete-schedule.output";
|
import { DeleteScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/delete-schedule.output";
|
||||||
import { GetDefaultScheduleOutput } from "@/ee/schedules/outputs/get-default-schedule.output";
|
import { GetDefaultScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-default-schedule.output";
|
||||||
import { GetScheduleOutput } from "@/ee/schedules/outputs/get-schedule.output";
|
import { GetScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-schedule.output";
|
||||||
import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output";
|
import { GetSchedulesOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-schedules.output";
|
||||||
import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output";
|
import { UpdateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/update-schedule.output";
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||||
|
import { VERSION_2024_04_15_VALUE } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
@@ -26,25 +27,25 @@ import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
|
|||||||
import { Throttle } from "@nestjs/throttler";
|
import { Throttle } from "@nestjs/throttler";
|
||||||
|
|
||||||
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { UpdateScheduleInput } from "@calcom/platform-types";
|
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
||||||
|
|
||||||
import { CreateScheduleInput } from "../inputs/create-schedule.input";
|
import { CreateScheduleInput_2024_04_15 } from "../inputs/create-schedule.input";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "schedules",
|
path: "/v2/schedules",
|
||||||
version: "2",
|
version: VERSION_2024_04_15_VALUE,
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||||
@DocsTags("Schedules")
|
@DocsTags("Schedules")
|
||||||
export class SchedulesController {
|
export class SchedulesController_2024_04_15 {
|
||||||
constructor(private readonly schedulesService: SchedulesService) {}
|
constructor(private readonly schedulesService: SchedulesService_2024_04_15) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@Permissions([SCHEDULE_WRITE])
|
@Permissions([SCHEDULE_WRITE])
|
||||||
async createSchedule(
|
async createSchedule(
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Body() bodySchedule: CreateScheduleInput
|
@Body() bodySchedule: CreateScheduleInput_2024_04_15
|
||||||
): Promise<CreateScheduleOutput> {
|
): Promise<CreateScheduleOutput_2024_04_15> {
|
||||||
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
|
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
|
||||||
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
||||||
|
|
||||||
@@ -56,8 +57,14 @@ export class SchedulesController {
|
|||||||
|
|
||||||
@Get("/default")
|
@Get("/default")
|
||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
@ApiResponse({ status: 200, description: "Returns the default schedule", type: GetDefaultScheduleOutput })
|
@ApiResponse({
|
||||||
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetDefaultScheduleOutput | null> {
|
status: 200,
|
||||||
|
description: "Returns the default schedule",
|
||||||
|
type: GetDefaultScheduleOutput_2024_04_15,
|
||||||
|
})
|
||||||
|
async getDefaultSchedule(
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<GetDefaultScheduleOutput_2024_04_15 | null> {
|
||||||
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
||||||
const scheduleFormatted = schedule
|
const scheduleFormatted = schedule
|
||||||
? await this.schedulesService.formatScheduleForAtom(user, schedule)
|
? await this.schedulesService.formatScheduleForAtom(user, schedule)
|
||||||
@@ -75,7 +82,7 @@ export class SchedulesController {
|
|||||||
async getSchedule(
|
async getSchedule(
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Param("scheduleId") scheduleId: number
|
@Param("scheduleId") scheduleId: number
|
||||||
): Promise<GetScheduleOutput> {
|
): Promise<GetScheduleOutput_2024_04_15> {
|
||||||
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
|
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
|
||||||
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
||||||
|
|
||||||
@@ -87,7 +94,7 @@ export class SchedulesController {
|
|||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput> {
|
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_04_15> {
|
||||||
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
||||||
const schedulesFormatted = await this.schedulesService.formatSchedulesForAtom(user, schedules);
|
const schedulesFormatted = await this.schedulesService.formatSchedulesForAtom(user, schedules);
|
||||||
|
|
||||||
@@ -102,9 +109,9 @@ export class SchedulesController {
|
|||||||
@Permissions([SCHEDULE_WRITE])
|
@Permissions([SCHEDULE_WRITE])
|
||||||
async updateSchedule(
|
async updateSchedule(
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Body() bodySchedule: UpdateScheduleInput,
|
@Body() bodySchedule: UpdateScheduleInput_2024_04_15,
|
||||||
@Param("scheduleId") scheduleId: string
|
@Param("scheduleId") scheduleId: string
|
||||||
): Promise<UpdateScheduleOutput> {
|
): Promise<UpdateScheduleOutput_2024_04_15> {
|
||||||
const updatedSchedule = await this.schedulesService.updateUserSchedule(
|
const updatedSchedule = await this.schedulesService.updateUserSchedule(
|
||||||
user,
|
user,
|
||||||
Number(scheduleId),
|
Number(scheduleId),
|
||||||
@@ -123,7 +130,7 @@ export class SchedulesController {
|
|||||||
async deleteSchedule(
|
async deleteSchedule(
|
||||||
@GetUser("id") userId: number,
|
@GetUser("id") userId: number,
|
||||||
@Param("scheduleId") scheduleId: number
|
@Param("scheduleId") scheduleId: number
|
||||||
): Promise<DeleteScheduleOutput> {
|
): Promise<DeleteScheduleOutput_2024_04_15> {
|
||||||
await this.schedulesService.deleteUserSchedule(userId, scheduleId);
|
await this.schedulesService.deleteUserSchedule(userId, scheduleId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
+1
-1
@@ -3,7 +3,7 @@ import { ApiProperty } from "@nestjs/swagger";
|
|||||||
import { Transform, TransformFnParams } from "class-transformer";
|
import { Transform, TransformFnParams } from "class-transformer";
|
||||||
import { IsArray, IsDate, IsNumber } from "class-validator";
|
import { IsArray, IsDate, IsNumber } from "class-validator";
|
||||||
|
|
||||||
export class CreateAvailabilityInput {
|
export class CreateAvailabilityInput_2024_04_15 {
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsNumber({}, { each: true })
|
@IsNumber({}, { each: true })
|
||||||
@ApiProperty({ example: [1, 2] })
|
@ApiProperty({ example: [1, 2] })
|
||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input";
|
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsArray, IsBoolean, IsTimeZone, IsOptional, IsString, ValidateNested } from "class-validator";
|
import { IsArray, IsBoolean, IsTimeZone, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
export class CreateScheduleInput {
|
export class CreateScheduleInput_2024_04_15 {
|
||||||
@IsString()
|
@IsString()
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@@ -11,9 +11,9 @@ export class CreateScheduleInput {
|
|||||||
|
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => CreateAvailabilityInput)
|
@Type(() => CreateAvailabilityInput_2024_04_15)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
availabilities?: CreateAvailabilityInput[];
|
availabilities?: CreateAvailabilityInput_2024_04_15[];
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isDefault!: boolean;
|
isDefault!: boolean;
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
export class CreateScheduleOutput {
|
export class CreateScheduleOutput_2024_04_15 {
|
||||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
+1
-1
@@ -3,7 +3,7 @@ import { IsEnum } from "class-validator";
|
|||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
export class DeleteScheduleOutput {
|
export class DeleteScheduleOutput_2024_04_15 {
|
||||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
export class GetDefaultScheduleOutput {
|
export class GetDefaultScheduleOutput_2024_04_15 {
|
||||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
export class GetScheduleOutput {
|
export class GetScheduleOutput_2024_04_15 {
|
||||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsArray, IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsArray, IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
export class GetSchedulesOutput {
|
export class GetSchedulesOutput_2024_04_15 {
|
||||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
+10
-10
@@ -1,7 +1,7 @@
|
|||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsBoolean, IsInt, IsOptional, IsString, ValidateNested, IsArray } from "class-validator";
|
import { IsBoolean, IsInt, IsOptional, IsString, ValidateNested, IsArray } from "class-validator";
|
||||||
|
|
||||||
class EventTypeModel {
|
class EventTypeModel_2024_04_15 {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ class EventTypeModel {
|
|||||||
eventName?: string | null;
|
eventName?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AvailabilityModel {
|
class AvailabilityModel_2024_04_15 {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ class AvailabilityModel {
|
|||||||
date?: Date | null;
|
date?: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScheduleModel {
|
class ScheduleModel_2024_04_15 {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@@ -62,21 +62,21 @@ class ScheduleModel {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => EventTypeModel)
|
@Type(() => EventTypeModel_2024_04_15)
|
||||||
@IsArray()
|
@IsArray()
|
||||||
eventType?: EventTypeModel[];
|
eventType?: EventTypeModel_2024_04_15[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => AvailabilityModel)
|
@Type(() => AvailabilityModel_2024_04_15)
|
||||||
@IsArray()
|
@IsArray()
|
||||||
availability?: AvailabilityModel[];
|
availability?: AvailabilityModel_2024_04_15[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdatedScheduleOutput {
|
export class UpdatedScheduleOutput_2024_04_15 {
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => ScheduleModel)
|
@Type(() => ScheduleModel_2024_04_15)
|
||||||
schedule!: ScheduleModel;
|
schedule!: ScheduleModel_2024_04_15;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isDefault!: boolean;
|
isDefault!: boolean;
|
||||||
File renamed without changes.
@@ -0,0 +1,20 @@
|
|||||||
|
import { UpdatedScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule-updated.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class UpdateScheduleOutput_2024_04_15 {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: UpdatedScheduleOutput_2024_04_15,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => UpdatedScheduleOutput_2024_04_15)
|
||||||
|
data!: UpdatedScheduleOutput_2024_04_15;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { SchedulesController_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/controllers/schedules.controller";
|
||||||
|
import { SchedulesRepository_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.repository";
|
||||||
|
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule, UsersModule, TokensModule],
|
||||||
|
providers: [SchedulesRepository_2024_04_15, SchedulesService_2024_04_15],
|
||||||
|
controllers: [SchedulesController_2024_04_15],
|
||||||
|
exports: [SchedulesService_2024_04_15, SchedulesRepository_2024_04_15],
|
||||||
|
})
|
||||||
|
export class SchedulesModule_2024_04_15 {}
|
||||||
+5
-5
@@ -1,18 +1,18 @@
|
|||||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
|
||||||
import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input";
|
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchedulesRepository {
|
export class SchedulesRepository_2024_04_15 {
|
||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
async createScheduleWithAvailabilities(
|
async createScheduleWithAvailabilities(
|
||||||
userId: number,
|
userId: number,
|
||||||
schedule: CreateScheduleInput,
|
schedule: CreateScheduleInput_2024_04_15,
|
||||||
availabilities: CreateAvailabilityInput[]
|
availabilities: CreateAvailabilityInput_2024_04_15[]
|
||||||
) {
|
) {
|
||||||
const createScheduleData: Prisma.ScheduleCreateInput = {
|
const createScheduleData: Prisma.ScheduleCreateInput = {
|
||||||
user: {
|
user: {
|
||||||
+28
-14
@@ -1,26 +1,25 @@
|
|||||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
|
||||||
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output";
|
||||||
import { AvailabilitiesService } from "@/modules/availabilities/availabilities.service";
|
import { SchedulesRepository_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.repository";
|
||||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { Schedule } from "@prisma/client";
|
import { Schedule } from "@prisma/client";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
import type { ScheduleWithAvailabilities } from "@calcom/platform-libraries";
|
import type { ScheduleWithAvailabilities } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { updateScheduleHandler } from "@calcom/platform-libraries";
|
import { updateScheduleHandler } from "@calcom/platform-libraries-0.0.2";
|
||||||
import {
|
import {
|
||||||
transformWorkingHoursForClient,
|
transformWorkingHoursForClient,
|
||||||
transformAvailabilityForClient,
|
transformAvailabilityForClient,
|
||||||
transformDateOverridesForClient,
|
transformDateOverridesForClient,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries-0.0.2";
|
||||||
import { UpdateScheduleInput } from "@calcom/platform-types";
|
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchedulesService {
|
export class SchedulesService_2024_04_15 {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly schedulesRepository: SchedulesRepository,
|
private readonly schedulesRepository: SchedulesRepository_2024_04_15,
|
||||||
private readonly availabilitiesService: AvailabilitiesService,
|
|
||||||
private readonly usersRepository: UsersRepository
|
private readonly usersRepository: UsersRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -34,10 +33,10 @@ export class SchedulesService {
|
|||||||
return this.createUserSchedule(userId, schedule);
|
return this.createUserSchedule(userId, schedule);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUserSchedule(userId: number, schedule: CreateScheduleInput) {
|
async createUserSchedule(userId: number, schedule: CreateScheduleInput_2024_04_15) {
|
||||||
const availabilities = schedule.availabilities?.length
|
const availabilities = schedule.availabilities?.length
|
||||||
? schedule.availabilities
|
? schedule.availabilities
|
||||||
: [this.availabilitiesService.getDefaultAvailabilityInput()];
|
: [this.getDefaultAvailabilityInput()];
|
||||||
|
|
||||||
const createdSchedule = await this.schedulesRepository.createScheduleWithAvailabilities(
|
const createdSchedule = await this.schedulesRepository.createScheduleWithAvailabilities(
|
||||||
userId,
|
userId,
|
||||||
@@ -76,7 +75,11 @@ export class SchedulesService {
|
|||||||
return this.schedulesRepository.getSchedulesByUserId(userId);
|
return this.schedulesRepository.getSchedulesByUserId(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUserSchedule(user: UserWithProfile, scheduleId: number, bodySchedule: UpdateScheduleInput) {
|
async updateUserSchedule(
|
||||||
|
user: UserWithProfile,
|
||||||
|
scheduleId: number,
|
||||||
|
bodySchedule: UpdateScheduleInput_2024_04_15
|
||||||
|
) {
|
||||||
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
||||||
|
|
||||||
if (!existingSchedule) {
|
if (!existingSchedule) {
|
||||||
@@ -158,4 +161,15 @@ export class SchedulesService {
|
|||||||
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDefaultAvailabilityInput(): CreateAvailabilityInput_2024_04_15 {
|
||||||
|
const startTime = new Date(new Date().setUTCHours(9, 0, 0, 0));
|
||||||
|
const endTime = new Date(new Date().setUTCHours(17, 0, 0, 0));
|
||||||
|
|
||||||
|
return {
|
||||||
|
days: [1, 2, 3, 4, 5],
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+238
@@ -0,0 +1,238 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
||||||
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.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 { User } from "@prisma/client";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||||
|
|
||||||
|
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_11 } from "@calcom/platform-constants";
|
||||||
|
import {
|
||||||
|
CreateScheduleInput_2024_06_11,
|
||||||
|
CreateScheduleOutput_2024_06_11,
|
||||||
|
GetScheduleOutput_2024_06_11,
|
||||||
|
GetSchedulesOutput_2024_06_11,
|
||||||
|
ScheduleOutput_2024_06_11,
|
||||||
|
UpdateScheduleOutput_2024_06_11,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
|
import { UpdateScheduleInput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
describe("Schedules Endpoints", () => {
|
||||||
|
describe("User Authentication", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let scheduleRepositoryFixture: SchedulesRepositoryFixture;
|
||||||
|
|
||||||
|
const userEmail = "schedules-controller-e2e@api.com";
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
const createScheduleInput: CreateScheduleInput_2024_06_11 = {
|
||||||
|
name: "work",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
isDefault: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultAvailability: CreateScheduleInput_2024_06_11["availability"] = [
|
||||||
|
{
|
||||||
|
days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
||||||
|
startTime: "09:00",
|
||||||
|
endTime: "17:00",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let createdSchedule: CreateScheduleOutput_2024_06_11["data"];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withAccessTokenAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_06_11],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.overrideGuard(PermissionsGuard)
|
||||||
|
.useValue({
|
||||||
|
canActivate: () => true,
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(userRepositoryFixture).toBeDefined();
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a default schedule", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/api/v2/schedules")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_11)
|
||||||
|
.send(createScheduleInput)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: CreateScheduleOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
createdSchedule = response.body.data;
|
||||||
|
|
||||||
|
const expectedSchedule = {
|
||||||
|
...createScheduleInput,
|
||||||
|
availability: defaultAvailability,
|
||||||
|
overrides: [],
|
||||||
|
};
|
||||||
|
outputScheduleMatchesExpected(createdSchedule, expectedSchedule, 1);
|
||||||
|
|
||||||
|
const scheduleOwner = createdSchedule.ownerId
|
||||||
|
? await userRepositoryFixture.get(createdSchedule.ownerId)
|
||||||
|
: null;
|
||||||
|
expect(scheduleOwner?.defaultScheduleId).toEqual(createdSchedule.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function outputScheduleMatchesExpected(
|
||||||
|
outputSchedule: ScheduleOutput_2024_06_11 | null,
|
||||||
|
expected: CreateScheduleInput_2024_06_11 & {
|
||||||
|
availability: CreateScheduleInput_2024_06_11["availability"];
|
||||||
|
} & {
|
||||||
|
overrides: CreateScheduleInput_2024_06_11["overrides"];
|
||||||
|
},
|
||||||
|
expectedAvailabilityLength: number
|
||||||
|
) {
|
||||||
|
expect(outputSchedule).toBeTruthy();
|
||||||
|
expect(outputSchedule?.name).toEqual(expected.name);
|
||||||
|
expect(outputSchedule?.timeZone).toEqual(expected.timeZone);
|
||||||
|
expect(outputSchedule?.isDefault).toEqual(expected.isDefault);
|
||||||
|
expect(outputSchedule?.availability.length).toEqual(expectedAvailabilityLength);
|
||||||
|
|
||||||
|
const outputScheduleAvailability = outputSchedule?.availability[0];
|
||||||
|
expect(outputScheduleAvailability).toBeDefined();
|
||||||
|
expect(outputScheduleAvailability?.days).toEqual(expected.availability?.[0].days);
|
||||||
|
expect(outputScheduleAvailability?.startTime).toEqual(expected.availability?.[0].startTime);
|
||||||
|
expect(outputScheduleAvailability?.endTime).toEqual(expected.availability?.[0].endTime);
|
||||||
|
|
||||||
|
expect(JSON.stringify(outputSchedule?.overrides)).toEqual(JSON.stringify(expected.overrides));
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should get default schedule", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get("/api/v2/schedules/default")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_11)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetScheduleOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const outputSchedule = responseBody.data;
|
||||||
|
|
||||||
|
const expectedSchedule = {
|
||||||
|
...createScheduleInput,
|
||||||
|
availability: defaultAvailability,
|
||||||
|
overrides: [],
|
||||||
|
};
|
||||||
|
outputScheduleMatchesExpected(outputSchedule, expectedSchedule, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get schedules", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/schedules`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_11)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const outputSchedule = responseBody.data[0];
|
||||||
|
|
||||||
|
const expectedSchedule = {
|
||||||
|
...createScheduleInput,
|
||||||
|
availability: defaultAvailability,
|
||||||
|
overrides: [],
|
||||||
|
};
|
||||||
|
outputScheduleMatchesExpected(outputSchedule, expectedSchedule, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update schedule name", async () => {
|
||||||
|
const newScheduleName = "updated-schedule-name";
|
||||||
|
|
||||||
|
const body: UpdateScheduleInput_2024_06_11 = {
|
||||||
|
name: newScheduleName,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/schedules/${createdSchedule.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_11)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.then((response: any) => {
|
||||||
|
const responseData: UpdateScheduleOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const responseSchedule = responseData.data;
|
||||||
|
|
||||||
|
const expectedSchedule = { ...createdSchedule, name: newScheduleName };
|
||||||
|
outputScheduleMatchesExpected(responseSchedule, expectedSchedule, 1);
|
||||||
|
|
||||||
|
createdSchedule = responseSchedule;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add overrides", async () => {
|
||||||
|
const overrides = [
|
||||||
|
{
|
||||||
|
date: "2026-05-05",
|
||||||
|
startTime: "10:00",
|
||||||
|
endTime: "12:00",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const body: UpdateScheduleInput_2024_06_11 = {
|
||||||
|
overrides,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/schedules/${createdSchedule.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_11)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.then((response: any) => {
|
||||||
|
const responseData: UpdateScheduleOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const responseSchedule = responseData.data;
|
||||||
|
|
||||||
|
const expectedSchedule = { ...createdSchedule, overrides };
|
||||||
|
outputScheduleMatchesExpected(responseSchedule, expectedSchedule, 1);
|
||||||
|
|
||||||
|
createdSchedule = responseSchedule;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should delete schedule", async () => {
|
||||||
|
return request(app.getHttpServer()).delete(`/api/v2/schedules/${createdSchedule.id}`).expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
try {
|
||||||
|
await scheduleRepositoryFixture.deleteById(createdSchedule.id);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||||
|
import { VERSION_2024_06_11_VALUE } from "@/lib/api-versions";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
import { Throttle } from "@nestjs/throttler";
|
||||||
|
|
||||||
|
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import {
|
||||||
|
CreateScheduleOutput_2024_06_11,
|
||||||
|
CreateScheduleInput_2024_06_11,
|
||||||
|
UpdateScheduleInput_2024_06_11,
|
||||||
|
GetScheduleOutput_2024_06_11,
|
||||||
|
UpdateScheduleOutput_2024_06_11,
|
||||||
|
GetDefaultScheduleOutput_2024_06_11,
|
||||||
|
DeleteScheduleOutput_2024_06_11,
|
||||||
|
GetSchedulesOutput_2024_06_11,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/schedules",
|
||||||
|
version: VERSION_2024_06_11_VALUE,
|
||||||
|
})
|
||||||
|
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||||
|
@DocsTags("Schedules")
|
||||||
|
export class SchedulesController_2024_06_11 {
|
||||||
|
constructor(private readonly schedulesService: SchedulesService_2024_06_11) {}
|
||||||
|
|
||||||
|
@Post("/")
|
||||||
|
@Permissions([SCHEDULE_WRITE])
|
||||||
|
async createSchedule(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Body() bodySchedule: CreateScheduleInput_2024_06_11
|
||||||
|
): Promise<CreateScheduleOutput_2024_06_11> {
|
||||||
|
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: schedule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/default")
|
||||||
|
@Permissions([SCHEDULE_READ])
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "Returns the default schedule",
|
||||||
|
type: GetDefaultScheduleOutput_2024_06_11,
|
||||||
|
})
|
||||||
|
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetScheduleOutput_2024_06_11> {
|
||||||
|
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: schedule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/:scheduleId")
|
||||||
|
@Permissions([SCHEDULE_READ])
|
||||||
|
@Throttle({ default: { limit: 10, ttl: 60000 } }) // allow 10 requests per minute (for :scheduleId)
|
||||||
|
async getSchedule(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Param("scheduleId") scheduleId: number
|
||||||
|
): Promise<GetScheduleOutput_2024_06_11> {
|
||||||
|
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: schedule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/")
|
||||||
|
@Permissions([SCHEDULE_READ])
|
||||||
|
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_06_11> {
|
||||||
|
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: schedules,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("/:scheduleId")
|
||||||
|
@Permissions([SCHEDULE_WRITE])
|
||||||
|
async updateSchedule(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Body() bodySchedule: UpdateScheduleInput_2024_06_11,
|
||||||
|
@Param("scheduleId") scheduleId: string
|
||||||
|
): Promise<UpdateScheduleOutput_2024_06_11> {
|
||||||
|
const updatedSchedule = await this.schedulesService.updateUserSchedule(
|
||||||
|
user.id,
|
||||||
|
Number(scheduleId),
|
||||||
|
bodySchedule
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: updatedSchedule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete("/:scheduleId")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Permissions([SCHEDULE_WRITE])
|
||||||
|
async deleteSchedule(
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("scheduleId") scheduleId: number
|
||||||
|
): Promise<DeleteScheduleOutput_2024_06_11> {
|
||||||
|
await this.schedulesService.deleteUserSchedule(userId, scheduleId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { SchedulesController_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/controllers/schedules.controller";
|
||||||
|
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||||
|
import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service";
|
||||||
|
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||||
|
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule, UsersModule, TokensModule],
|
||||||
|
providers: [
|
||||||
|
SchedulesRepository_2024_06_11,
|
||||||
|
SchedulesService_2024_06_11,
|
||||||
|
InputSchedulesService_2024_06_11,
|
||||||
|
OutputSchedulesService_2024_06_11,
|
||||||
|
],
|
||||||
|
controllers: [SchedulesController_2024_06_11],
|
||||||
|
exports: [SchedulesService_2024_06_11, SchedulesRepository_2024_06_11],
|
||||||
|
})
|
||||||
|
export class SchedulesModule_2024_06_11 {}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
import type { CreateScheduleInput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
type InputScheduleAvailabilityTransformed = {
|
||||||
|
days: number[];
|
||||||
|
startTime: Date;
|
||||||
|
endTime: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
type InputScheduleOverrideTransformed = {
|
||||||
|
date: Date;
|
||||||
|
startTime: Date;
|
||||||
|
endTime: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
type InputScheduleTransformed = Omit<CreateScheduleInput_2024_06_11, "availability" | "overrides"> & {
|
||||||
|
availability: InputScheduleAvailabilityTransformed[];
|
||||||
|
overrides: InputScheduleOverrideTransformed[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SchedulesRepository_2024_06_11 {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
|
async createSchedule(userId: number, schedule: Omit<InputScheduleTransformed, "isDefault">) {
|
||||||
|
const { availability, overrides } = schedule;
|
||||||
|
|
||||||
|
const createScheduleData: Prisma.ScheduleCreateInput = {
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: schedule.name,
|
||||||
|
timeZone: schedule.timeZone,
|
||||||
|
};
|
||||||
|
|
||||||
|
const availabilitiesAndOverrides: Prisma.AvailabilityCreateManyInput[] = [];
|
||||||
|
|
||||||
|
if (availability && availability.length > 0) {
|
||||||
|
availability.forEach((availability) => {
|
||||||
|
availabilitiesAndOverrides.push({
|
||||||
|
days: availability.days,
|
||||||
|
startTime: availability.startTime,
|
||||||
|
endTime: availability.endTime,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides && overrides.length > 0) {
|
||||||
|
overrides.forEach((override) => {
|
||||||
|
availabilitiesAndOverrides.push({
|
||||||
|
date: override.date,
|
||||||
|
startTime: override.startTime,
|
||||||
|
endTime: override.endTime,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availabilitiesAndOverrides.length > 0) {
|
||||||
|
createScheduleData.availability = {
|
||||||
|
createMany: {
|
||||||
|
data: availabilitiesAndOverrides,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdSchedule = await this.dbWrite.prisma.schedule.create({
|
||||||
|
data: {
|
||||||
|
...createScheduleData,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
availability: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdSchedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getScheduleById(scheduleId: number) {
|
||||||
|
const schedule = await this.dbRead.prisma.schedule.findUnique({
|
||||||
|
where: {
|
||||||
|
id: scheduleId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
availability: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return schedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSchedule(
|
||||||
|
userId: number,
|
||||||
|
scheduleId: number,
|
||||||
|
schedule: Partial<Omit<InputScheduleTransformed, "isDefault">>
|
||||||
|
) {
|
||||||
|
const { availability, overrides } = schedule;
|
||||||
|
|
||||||
|
const updateScheduleData: Prisma.ScheduleUpdateInput = {
|
||||||
|
name: schedule.name,
|
||||||
|
timeZone: schedule.timeZone,
|
||||||
|
};
|
||||||
|
|
||||||
|
const availabilitiesAndOverrides: Prisma.AvailabilityCreateManyInput[] = [];
|
||||||
|
|
||||||
|
const deleteConditions = [];
|
||||||
|
if (availability) {
|
||||||
|
// note(Lauris): availabilities and overrides are stored in the same "Availability" table,
|
||||||
|
// but availabilities have "date" field as null, while overrides have it as not null, so delete
|
||||||
|
// condition below results in deleting only rows from Availability table that are availabilities.
|
||||||
|
deleteConditions.push({
|
||||||
|
scheduleId: { equals: scheduleId },
|
||||||
|
date: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides) {
|
||||||
|
// note(Lauris): availabilities and overrides are stored in the same "Availability" table,
|
||||||
|
// but overrides have "date" field as not-null, while availabilities have it as null, so delete
|
||||||
|
// condition below results in deleting only rows from Availability table that are overrides.
|
||||||
|
deleteConditions.push({
|
||||||
|
scheduleId: { equals: scheduleId },
|
||||||
|
NOT: { date: null },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availability && availability.length > 0) {
|
||||||
|
availability.forEach((availability) => {
|
||||||
|
availabilitiesAndOverrides.push({
|
||||||
|
days: availability.days,
|
||||||
|
startTime: availability.startTime,
|
||||||
|
endTime: availability.endTime,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides && overrides.length > 0) {
|
||||||
|
overrides.forEach((override) => {
|
||||||
|
availabilitiesAndOverrides.push({
|
||||||
|
date: override.date,
|
||||||
|
startTime: override.startTime,
|
||||||
|
endTime: override.endTime,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availabilitiesAndOverrides.length > 0) {
|
||||||
|
updateScheduleData.availability = {
|
||||||
|
deleteMany: deleteConditions,
|
||||||
|
createMany: {
|
||||||
|
data: availabilitiesAndOverrides,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSchedule = await this.dbWrite.prisma.schedule.update({
|
||||||
|
where: {
|
||||||
|
id: scheduleId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...updateScheduleData,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
availability: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedSchedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSchedulesByUserId(userId: number) {
|
||||||
|
const schedules = await this.dbRead.prisma.schedule.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
availability: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return schedules;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteScheduleById(scheduleId: number) {
|
||||||
|
return this.dbWrite.prisma.schedule.delete({
|
||||||
|
where: {
|
||||||
|
id: scheduleId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserSchedulesCount(userId: number) {
|
||||||
|
return this.dbRead.prisma.schedule.count({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
transformApiScheduleOverrides,
|
||||||
|
transformApiScheduleAvailability,
|
||||||
|
} from "@calcom/platform-libraries-0.0.4";
|
||||||
|
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
import { ScheduleOverrideInput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class InputSchedulesService_2024_06_11 {
|
||||||
|
transformInputCreateSchedule(inputSchedule: CreateScheduleInput_2024_06_11) {
|
||||||
|
const defaultAvailability: ScheduleAvailabilityInput_2024_06_11[] = [
|
||||||
|
{
|
||||||
|
days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
||||||
|
startTime: "09:00",
|
||||||
|
endTime: "17:00",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const defaultOverrides: ScheduleOverrideInput_2024_06_11[] = [];
|
||||||
|
|
||||||
|
const availability = this.transformInputScheduleAvailability(
|
||||||
|
inputSchedule.availability || defaultAvailability
|
||||||
|
);
|
||||||
|
const overrides = this.transformInputOverrides(inputSchedule.overrides || defaultOverrides);
|
||||||
|
|
||||||
|
const internalCreateSchedule = {
|
||||||
|
...inputSchedule,
|
||||||
|
availability,
|
||||||
|
overrides,
|
||||||
|
};
|
||||||
|
|
||||||
|
return internalCreateSchedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
transformInputScheduleAvailability(inputAvailability: ScheduleAvailabilityInput_2024_06_11[]) {
|
||||||
|
return transformApiScheduleAvailability(inputAvailability);
|
||||||
|
}
|
||||||
|
|
||||||
|
transformInputOverrides(inputOverrides: ScheduleOverrideInput_2024_06_11[]) {
|
||||||
|
return transformApiScheduleOverrides(inputOverrides);
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import type { Availability, Schedule } from "@prisma/client";
|
||||||
|
|
||||||
|
import { WeekDay } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OutputSchedulesService_2024_06_11 {
|
||||||
|
constructor(private readonly usersRepository: UsersRepository) {}
|
||||||
|
|
||||||
|
async getResponseSchedule(databaseSchedule: Schedule & { availability: Availability[] }) {
|
||||||
|
if (!databaseSchedule.timeZone) {
|
||||||
|
throw new Error("Failed to create schedule because its timezone is not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerDefaultScheduleId = await this.usersRepository.getUserScheduleDefaultId(
|
||||||
|
databaseSchedule.userId
|
||||||
|
);
|
||||||
|
|
||||||
|
const createdScheduleAvailabilities = databaseSchedule.availability.filter(
|
||||||
|
(availability) => !!availability.days.length
|
||||||
|
);
|
||||||
|
const createdScheduleOverrides = databaseSchedule.availability.filter((override) => !!override.date);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: databaseSchedule.id,
|
||||||
|
ownerId: databaseSchedule.userId,
|
||||||
|
name: databaseSchedule.name,
|
||||||
|
timeZone: databaseSchedule.timeZone,
|
||||||
|
availability: createdScheduleAvailabilities.map((availability) => ({
|
||||||
|
days: availability.days.map(transformNumberToDay),
|
||||||
|
startTime: this.padHoursMinutesWithZeros(
|
||||||
|
availability.startTime.getUTCHours() + ":" + availability.startTime.getUTCMinutes()
|
||||||
|
),
|
||||||
|
endTime: this.padHoursMinutesWithZeros(
|
||||||
|
availability.endTime.getUTCHours() + ":" + availability.endTime.getUTCMinutes()
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
isDefault: databaseSchedule.id === ownerDefaultScheduleId,
|
||||||
|
overrides: createdScheduleOverrides.map((override) => ({
|
||||||
|
date:
|
||||||
|
override.date?.getUTCFullYear() +
|
||||||
|
"-" +
|
||||||
|
(override.date ? override.date.getUTCMonth() + 1 : "").toString().padStart(2, "0") +
|
||||||
|
"-" +
|
||||||
|
override.date?.getUTCDate().toString().padStart(2, "0"),
|
||||||
|
startTime: this.padHoursMinutesWithZeros(
|
||||||
|
override.startTime.getUTCHours() + ":" + override.startTime.getUTCMinutes()
|
||||||
|
),
|
||||||
|
endTime: this.padHoursMinutesWithZeros(
|
||||||
|
override.endTime.getUTCHours() + ":" + override.endTime.getUTCMinutes()
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
padHoursMinutesWithZeros(hhMM: string) {
|
||||||
|
const [hours, minutes] = hhMM.split(":");
|
||||||
|
|
||||||
|
const formattedHours = hours.padStart(2, "0");
|
||||||
|
const formattedMinutes = minutes.padStart(2, "0");
|
||||||
|
|
||||||
|
return `${formattedHours}:${formattedMinutes}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformNumberToDay(day: number): WeekDay {
|
||||||
|
const weekMap: { [key: number]: WeekDay } = {
|
||||||
|
0: "Sunday",
|
||||||
|
1: "Monday",
|
||||||
|
2: "Tuesday",
|
||||||
|
3: "Wednesday",
|
||||||
|
4: "Thursday",
|
||||||
|
5: "Friday",
|
||||||
|
6: "Saturday",
|
||||||
|
};
|
||||||
|
return weekMap[day];
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||||
|
import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service";
|
||||||
|
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
import { Schedule } from "@prisma/client";
|
||||||
|
|
||||||
|
import { CreateScheduleInput_2024_06_11, ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
import { UpdateScheduleInput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SchedulesService_2024_06_11 {
|
||||||
|
constructor(
|
||||||
|
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
|
||||||
|
private readonly inputSchedulesService: InputSchedulesService_2024_06_11,
|
||||||
|
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11,
|
||||||
|
private readonly usersRepository: UsersRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createUserDefaultSchedule(userId: number, timeZone: string) {
|
||||||
|
const defaultSchedule = {
|
||||||
|
isDefault: true,
|
||||||
|
name: "Default schedule",
|
||||||
|
timeZone,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.createUserSchedule(userId, defaultSchedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUserSchedule(
|
||||||
|
userId: number,
|
||||||
|
scheduleInput: CreateScheduleInput_2024_06_11
|
||||||
|
): Promise<ScheduleOutput_2024_06_11> {
|
||||||
|
const schedule = this.inputSchedulesService.transformInputCreateSchedule(scheduleInput);
|
||||||
|
|
||||||
|
const createdSchedule = await this.schedulesRepository.createSchedule(userId, schedule);
|
||||||
|
|
||||||
|
if (schedule.isDefault) {
|
||||||
|
await this.usersRepository.setDefaultSchedule(userId, createdSchedule.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.outputSchedulesService.getResponseSchedule(createdSchedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserScheduleDefault(userId: number) {
|
||||||
|
const user = await this.usersRepository.findById(userId);
|
||||||
|
|
||||||
|
if (!user?.defaultScheduleId) return null;
|
||||||
|
|
||||||
|
const defaultSchedule = await this.schedulesRepository.getScheduleById(user.defaultScheduleId);
|
||||||
|
|
||||||
|
if (!defaultSchedule) return null;
|
||||||
|
return this.outputSchedulesService.getResponseSchedule(defaultSchedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserSchedule(userId: number, scheduleId: number) {
|
||||||
|
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
||||||
|
|
||||||
|
if (!existingSchedule) {
|
||||||
|
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkUserOwnsSchedule(userId, existingSchedule);
|
||||||
|
|
||||||
|
return this.outputSchedulesService.getResponseSchedule(existingSchedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserSchedules(userId: number) {
|
||||||
|
const schedules = await this.schedulesRepository.getSchedulesByUserId(userId);
|
||||||
|
return Promise.all(
|
||||||
|
schedules.map(async (schedule) => {
|
||||||
|
return this.outputSchedulesService.getResponseSchedule(schedule);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUserSchedule(userId: number, scheduleId: number, bodySchedule: UpdateScheduleInput_2024_06_11) {
|
||||||
|
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
||||||
|
|
||||||
|
if (!existingSchedule) {
|
||||||
|
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkUserOwnsSchedule(userId, existingSchedule);
|
||||||
|
|
||||||
|
const availability = bodySchedule.availability
|
||||||
|
? this.inputSchedulesService.transformInputScheduleAvailability(bodySchedule.availability)
|
||||||
|
: undefined;
|
||||||
|
const overrides = bodySchedule.overrides
|
||||||
|
? this.inputSchedulesService.transformInputOverrides(bodySchedule.overrides)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (bodySchedule.isDefault) {
|
||||||
|
await this.usersRepository.setDefaultSchedule(userId, scheduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSchedule = await this.schedulesRepository.updateSchedule(userId, scheduleId, {
|
||||||
|
...bodySchedule,
|
||||||
|
availability,
|
||||||
|
overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.outputSchedulesService.getResponseSchedule(updatedSchedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUserSchedule(userId: number, scheduleId: number) {
|
||||||
|
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
||||||
|
|
||||||
|
if (!existingSchedule) {
|
||||||
|
throw new BadRequestException(`Schedule with ID=${scheduleId} does not exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkUserOwnsSchedule(userId, existingSchedule);
|
||||||
|
|
||||||
|
return this.schedulesRepository.deleteScheduleById(scheduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">) {
|
||||||
|
if (userId !== schedule.userId) {
|
||||||
|
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { ArgumentsHost, Catch, Logger, HttpStatus } from "@nestjs/common";
|
|
||||||
import { BaseExceptionFilter } from "@nestjs/core";
|
|
||||||
import * as Sentry from "@sentry/node";
|
|
||||||
import { Request } from "express";
|
|
||||||
|
|
||||||
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
|
||||||
import { Response } from "@calcom/platform-types";
|
|
||||||
|
|
||||||
@Catch()
|
|
||||||
export class SentryFilter extends BaseExceptionFilter {
|
|
||||||
private readonly logger = new Logger("SentryExceptionFilter");
|
|
||||||
|
|
||||||
handleUnknownError(exception: any, host: ArgumentsHost): void {
|
|
||||||
const ctx = host.switchToHttp();
|
|
||||||
const response = ctx.getResponse<Response>();
|
|
||||||
const request = ctx.getRequest<Request>();
|
|
||||||
const requestId = request.headers["X-Request-Id"];
|
|
||||||
|
|
||||||
this.logger.error(`Sentry Exception Filter: ${exception?.message}`, {
|
|
||||||
exception,
|
|
||||||
body: request.body,
|
|
||||||
headers: request.headers,
|
|
||||||
url: request.url,
|
|
||||||
method: request.method,
|
|
||||||
requestId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// capture if client has been init
|
|
||||||
if (Boolean(Sentry.getCurrentHub().getClient())) {
|
|
||||||
Sentry.captureException(exception);
|
|
||||||
}
|
|
||||||
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
|
||||||
status: ERROR_STATUS,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
path: request.url,
|
|
||||||
error: { code: INTERNAL_SERVER_ERROR, message: "Internal server error." },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
|||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
|
|
||||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
import { TRPCError } from "@calcom/platform-libraries";
|
import { TRPCError } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { Response } from "@calcom/platform-types";
|
import { Response } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Catch(TRPCError)
|
@Catch(TRPCError)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { getEnv } from "@/env";
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
|
||||||
|
if (process.env.SENTRY_DSN) {
|
||||||
|
// Ensure to call this before requiring any other modules!
|
||||||
|
Sentry.init({
|
||||||
|
dsn: getEnv("SENTRY_DSN"),
|
||||||
|
// Add Performance Monitoring by setting tracesSampleRate
|
||||||
|
// We recommend adjusting this value in production
|
||||||
|
// todo: Evaluate? tracesSampleRate: 1.0
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { VersionValue } from "@nestjs/common/interfaces";
|
||||||
|
|
||||||
|
import {
|
||||||
|
API_VERSIONS,
|
||||||
|
VERSION_2024_04_15,
|
||||||
|
VERSION_2024_05_21,
|
||||||
|
VERSION_2024_06_11,
|
||||||
|
} from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export const API_VERSIONS_VALUES: VersionValue = API_VERSIONS as unknown as VersionValue;
|
||||||
|
export const VERSION_2024_06_11_VALUE: VersionValue = VERSION_2024_06_11 as unknown as VersionValue;
|
||||||
|
export const VERSION_2024_05_21_VALUE: VersionValue = VERSION_2024_05_21 as unknown as VersionValue;
|
||||||
|
export const VERSION_2024_04_15_VALUE: VersionValue = VERSION_2024_04_15 as unknown as VersionValue;
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { AvailabilitiesService } from "@/modules/availabilities/availabilities.service";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
|
||||||
import { Module } from "@nestjs/common";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [PrismaModule],
|
|
||||||
providers: [AvailabilitiesService],
|
|
||||||
exports: [AvailabilitiesService],
|
|
||||||
})
|
|
||||||
export class AvailabilitiesModule {}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input";
|
|
||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AvailabilitiesService {
|
|
||||||
getDefaultAvailabilityInput(): CreateAvailabilityInput {
|
|
||||||
const startTime = new Date(new Date().setUTCHours(9, 0, 0, 0));
|
|
||||||
const endTime = new Date(new Date().setUTCHours(17, 0, 0, 0));
|
|
||||||
|
|
||||||
return {
|
|
||||||
days: [1, 2, 3, 4, 5],
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AppConfig } from "@/config/type";
|
import { AppConfig } from "@/config/type";
|
||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||||
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
||||||
@@ -29,8 +30,8 @@ import { Stripe } from "stripe";
|
|||||||
import { ApiResponse } from "@calcom/platform-types";
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/billing",
|
path: "/v2/billing",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@ApiExcludeController(true)
|
@ApiExcludeController(true)
|
||||||
export class BillingController {
|
export class BillingController {
|
||||||
|
|||||||
+6
-4
@@ -1,3 +1,4 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { CreateManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
|
import { CreateManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
|
||||||
import { GetManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-user.output";
|
import { GetManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-user.output";
|
||||||
import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output";
|
import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output";
|
||||||
@@ -21,7 +22,6 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
Param,
|
Param,
|
||||||
Patch,
|
Patch,
|
||||||
BadRequestException,
|
|
||||||
Delete,
|
Delete,
|
||||||
Query,
|
Query,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
@@ -33,8 +33,8 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
|||||||
import { Pagination } from "@calcom/platform-types";
|
import { Pagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "oauth-clients/:clientId/users",
|
path: "/v2/oauth-clients/:clientId/users",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@UseGuards(OAuthClientCredentialsGuard)
|
@UseGuards(OAuthClientCredentialsGuard)
|
||||||
@DocsTags("Managed users")
|
@DocsTags("Managed users")
|
||||||
@@ -91,6 +91,7 @@ export class OAuthClientUsersController {
|
|||||||
data: {
|
data: {
|
||||||
user: this.getResponseUser(user),
|
user: this.getResponseUser(user),
|
||||||
accessToken: tokens.accessToken,
|
accessToken: tokens.accessToken,
|
||||||
|
accessTokenExpiresAt: tokens.accessTokenExpiresAt.valueOf(),
|
||||||
refreshToken: tokens.refreshToken,
|
refreshToken: tokens.refreshToken,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -155,7 +156,7 @@ export class OAuthClientUsersController {
|
|||||||
|
|
||||||
const { id } = await this.validateManagedUserOwnership(oAuthClientId, userId);
|
const { id } = await this.validateManagedUserOwnership(oAuthClientId, userId);
|
||||||
|
|
||||||
const { accessToken, refreshToken } = await this.tokensRepository.createOAuthTokens(
|
const { accessToken, refreshToken, accessTokenExpiresAt } = await this.tokensRepository.createOAuthTokens(
|
||||||
oAuthClientId,
|
oAuthClientId,
|
||||||
id,
|
id,
|
||||||
true
|
true
|
||||||
@@ -166,6 +167,7 @@ export class OAuthClientUsersController {
|
|||||||
data: {
|
data: {
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
accessTokenExpiresAt: accessTokenExpiresAt.valueOf(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -1,7 +1,7 @@
|
|||||||
import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output";
|
import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
import { IsEnum, IsNumber, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
@@ -18,6 +18,9 @@ class CreateManagedUserData {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
refreshToken!: string;
|
refreshToken!: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
accessTokenExpiresAt!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateManagedUserOutput {
|
export class CreateManagedUserOutput {
|
||||||
|
|||||||
+3
-2
@@ -1,4 +1,5 @@
|
|||||||
import { getEnv } from "@/env";
|
import { getEnv } from "@/env";
|
||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||||
@@ -46,8 +47,8 @@ const AUTH_DOCUMENTATION = `⚠️ First, this endpoint requires \`Cookie: next-
|
|||||||
Second, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.`;
|
Second, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.`;
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "oauth-clients",
|
path: "/v2/oauth-clients",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
|
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
|
||||||
@DocsExcludeController(getEnv("NODE_ENV") === "production")
|
@DocsExcludeController(getEnv("NODE_ENV") === "production")
|
||||||
|
|||||||
+12
-8
@@ -1,4 +1,5 @@
|
|||||||
import { getEnv } from "@/env";
|
import { getEnv } from "@/env";
|
||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||||
import { KeysResponseDto } from "@/modules/oauth-clients/controllers/oauth-flow/responses/KeysResponse.dto";
|
import { KeysResponseDto } from "@/modules/oauth-clients/controllers/oauth-flow/responses/KeysResponse.dto";
|
||||||
@@ -33,8 +34,8 @@ import { Response as ExpressResponse } from "express";
|
|||||||
import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "oauth/:clientId",
|
path: "/v2/oauth/:clientId",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@DocsExcludeController(getEnv("NODE_ENV") === "production")
|
@DocsExcludeController(getEnv("NODE_ENV") === "production")
|
||||||
@DocsTags("OAuth - development only")
|
@DocsTags("OAuth - development only")
|
||||||
@@ -117,16 +118,18 @@ export class OAuthFlowController {
|
|||||||
throw new BadRequestException("Missing 'Bearer' Authorization header.");
|
throw new BadRequestException("Missing 'Bearer' Authorization header.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { accessToken, refreshToken } = await this.oAuthFlowService.exchangeAuthorizationToken(
|
const { accessToken, refreshToken, accessTokenExpiresAt } =
|
||||||
authorizeEndpointCode,
|
await this.oAuthFlowService.exchangeAuthorizationToken(
|
||||||
clientId,
|
authorizeEndpointCode,
|
||||||
body.clientSecret
|
clientId,
|
||||||
);
|
body.clientSecret
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: {
|
data: {
|
||||||
accessToken,
|
accessToken,
|
||||||
|
accessTokenExpiresAt: accessTokenExpiresAt.valueOf(),
|
||||||
refreshToken,
|
refreshToken,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -140,7 +143,7 @@ export class OAuthFlowController {
|
|||||||
@Headers(X_CAL_SECRET_KEY) secretKey: string,
|
@Headers(X_CAL_SECRET_KEY) secretKey: string,
|
||||||
@Body() body: RefreshTokenInput
|
@Body() body: RefreshTokenInput
|
||||||
): Promise<KeysResponseDto> {
|
): Promise<KeysResponseDto> {
|
||||||
const { accessToken, refreshToken } = await this.oAuthFlowService.refreshToken(
|
const { accessToken, refreshToken, accessTokenExpiresAt } = await this.oAuthFlowService.refreshToken(
|
||||||
clientId,
|
clientId,
|
||||||
secretKey,
|
secretKey,
|
||||||
body.refreshToken
|
body.refreshToken
|
||||||
@@ -150,6 +153,7 @@ export class OAuthFlowController {
|
|||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: {
|
data: {
|
||||||
accessToken: accessToken,
|
accessToken: accessToken,
|
||||||
|
accessTokenExpiresAt: accessTokenExpiresAt.valueOf(),
|
||||||
refreshToken: refreshToken,
|
refreshToken: refreshToken,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,6 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { ValidateNested, IsEnum, IsString, IsNotEmptyObject } from "class-validator";
|
import { ValidateNested, IsEnum, IsString, IsNotEmptyObject, IsNumber } from "class-validator";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
@@ -16,6 +16,9 @@ class KeysDto {
|
|||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
refreshToken!: string;
|
refreshToken!: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
accessTokenExpiresAt!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KeysResponseDto {
|
export class KeysResponseDto {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
||||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
import { AuthModule } from "@/modules/auth/auth.module";
|
import { AuthModule } from "@/modules/auth/auth.module";
|
||||||
import { BillingModule } from "@/modules/billing/billing.module";
|
import { BillingModule } from "@/modules/billing/billing.module";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
@@ -32,7 +32,7 @@ import { Global, Module } from "@nestjs/common";
|
|||||||
OrganizationsModule,
|
OrganizationsModule,
|
||||||
StripeModule,
|
StripeModule,
|
||||||
BillingModule,
|
BillingModule,
|
||||||
SchedulesModule,
|
SchedulesModule_2024_04_15,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
OAuthClientRepository,
|
OAuthClientRepository,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||||
@@ -7,7 +7,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
|||||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
|
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries-0.0.2";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OAuthClientUsersService {
|
export class OAuthClientUsersService {
|
||||||
@@ -15,7 +15,7 @@ export class OAuthClientUsersService {
|
|||||||
private readonly userRepository: UsersRepository,
|
private readonly userRepository: UsersRepository,
|
||||||
private readonly tokensRepository: TokensRepository,
|
private readonly tokensRepository: TokensRepository,
|
||||||
private readonly eventTypesService: EventTypesService,
|
private readonly eventTypesService: EventTypesService,
|
||||||
private readonly schedulesService: SchedulesService
|
private readonly schedulesService: SchedulesService_2024_04_15
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createOauthClientUser(
|
async createOauthClientUser(
|
||||||
@@ -62,7 +62,7 @@ export class OAuthClientUsersService {
|
|||||||
await this.userRepository.update(user.id, { name: body.name ?? user.username ?? undefined });
|
await this.userRepository.update(user.id, { name: body.name ?? user.username ?? undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { accessToken, refreshToken } = await this.tokensRepository.createOAuthTokens(
|
const { accessToken, refreshToken, accessTokenExpiresAt } = await this.tokensRepository.createOAuthTokens(
|
||||||
oAuthClientId,
|
oAuthClientId,
|
||||||
user.id
|
user.id
|
||||||
);
|
);
|
||||||
@@ -78,6 +78,7 @@ export class OAuthClientUsersService {
|
|||||||
user,
|
user,
|
||||||
tokens: {
|
tokens: {
|
||||||
accessToken,
|
accessToken,
|
||||||
|
accessTokenExpiresAt,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export class OAuthFlowService {
|
|||||||
tokenId: string,
|
tokenId: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
clientSecret: string
|
clientSecret: string
|
||||||
): Promise<{ accessToken: string; refreshToken: string }> {
|
): Promise<{ accessToken: string; refreshToken: string; accessTokenExpiresAt: Date }> {
|
||||||
const oauthClient = await this.oAuthClientRepository.getOAuthClientWithAuthTokens(
|
const oauthClient = await this.oAuthClientRepository.getOAuthClientWithAuthTokens(
|
||||||
tokenId,
|
tokenId,
|
||||||
clientId,
|
clientId,
|
||||||
@@ -120,7 +120,7 @@ export class OAuthFlowService {
|
|||||||
throw new BadRequestException("Invalid Authorization Token.");
|
throw new BadRequestException("Invalid Authorization Token.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { accessToken, refreshToken } = await this.tokensRepository.createOAuthTokens(
|
const { accessToken, refreshToken, accessTokenExpiresAt } = await this.tokensRepository.createOAuthTokens(
|
||||||
clientId,
|
clientId,
|
||||||
authorizationToken.owner.id
|
authorizationToken.owner.id
|
||||||
);
|
);
|
||||||
@@ -129,6 +129,7 @@ export class OAuthFlowService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
|
accessTokenExpiresAt,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -158,6 +159,7 @@ export class OAuthFlowService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: accessToken.secret,
|
accessToken: accessToken.secret,
|
||||||
|
accessTokenExpiresAt: accessToken.expiresAt,
|
||||||
refreshToken: refreshToken.secret,
|
refreshToken: refreshToken.secret,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
import { SlotsService } from "@/modules/slots/services/slots.service";
|
import { SlotsService } from "@/modules/slots/services/slots.service";
|
||||||
import { Query, Body, Controller, Get, Delete, Post, Req, Res, UseGuards } from "@nestjs/common";
|
import { Query, Body, Controller, Get, Delete, Post, Req, Res, UseGuards } from "@nestjs/common";
|
||||||
@@ -5,14 +6,14 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
|
|||||||
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { getAvailableSlots } from "@calcom/platform-libraries";
|
import { getAvailableSlots } from "@calcom/platform-libraries-0.0.2";
|
||||||
import type { AvailableSlotsType } from "@calcom/platform-libraries";
|
import type { AvailableSlotsType } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
|
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
|
||||||
import { ApiResponse, GetAvailableSlotsInput } from "@calcom/platform-types";
|
import { ApiResponse, GetAvailableSlotsInput } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "slots",
|
path: "/v2/slots",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@DocsTags("Slots")
|
@DocsTags("Slots")
|
||||||
export class SlotsController {
|
export class SlotsController {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries";
|
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { ReserveSlotInput } from "@calcom/platform-types";
|
import { ReserveSlotInput } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { TimezonesService } from "@/modules/timezones/services/timezones.service";
|
import { TimezonesService } from "@/modules/timezones/services/timezones.service";
|
||||||
import { Controller, Get } from "@nestjs/common";
|
import { Controller, Get } from "@nestjs/common";
|
||||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import type { CityTimezones } from "@calcom/platform-libraries";
|
import type { CityTimezones } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { ApiResponse } from "@calcom/platform-types";
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "timezones",
|
path: "/v2/timezones",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@DocsTags("Timezones")
|
@DocsTags("Timezones")
|
||||||
export class TimezonesController {
|
export class TimezonesController {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { RedisService } from "@/modules/redis/redis.service";
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import { cityTimezonesHandler } from "@calcom/platform-libraries";
|
import { cityTimezonesHandler } from "@calcom/platform-libraries-0.0.2";
|
||||||
import type { CityTimezones } from "@calcom/platform-libraries";
|
import type { CityTimezones } from "@calcom/platform-libraries-0.0.2";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TimezonesService {
|
export class TimezonesService {
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export class TokensRepository {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: accessToken.secret,
|
accessToken: accessToken.secret,
|
||||||
|
accessTokenExpiresAt: accessToken.expiresAt,
|
||||||
refreshToken: refreshToken.secret,
|
refreshToken: refreshToken.secret,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,14 @@ export class UsersRepository {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserScheduleDefaultId(userId: number) {
|
||||||
|
const user = await this.findById(userId);
|
||||||
|
|
||||||
|
if (!user?.defaultScheduleId) return null;
|
||||||
|
|
||||||
|
return user?.defaultScheduleId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function capitalizeTimezone(timezone: string) {
|
function capitalizeTimezone(timezone: string) {
|
||||||
|
|||||||
@@ -634,7 +634,7 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "number"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -808,14 +808,14 @@
|
|||||||
},
|
},
|
||||||
"/v2/schedules": {
|
"/v2/schedules": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "SchedulesController_createSchedule",
|
"operationId": "SchedulesController_2024_06_11_createSchedule",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/CreateScheduleInput"
|
"$ref": "#/components/schemas/CreateScheduleInput_2024_06_11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -826,7 +826,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/CreateScheduleOutput"
|
"$ref": "#/components/schemas/CreateScheduleOutput_2024_06_11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -837,7 +837,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "SchedulesController_getSchedules",
|
"operationId": "SchedulesController_2024_06_11_getSchedules",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@@ -845,7 +845,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/GetSchedulesOutput"
|
"$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -858,7 +858,7 @@
|
|||||||
},
|
},
|
||||||
"/v2/schedules/default": {
|
"/v2/schedules/default": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "SchedulesController_getDefaultSchedule",
|
"operationId": "SchedulesController_2024_06_11_getDefaultSchedule",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@@ -866,7 +866,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/GetDefaultScheduleOutput"
|
"$ref": "#/components/schemas/GetDefaultScheduleOutput_2024_06_11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -879,7 +879,7 @@
|
|||||||
},
|
},
|
||||||
"/v2/schedules/{scheduleId}": {
|
"/v2/schedules/{scheduleId}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "SchedulesController_getSchedule",
|
"operationId": "SchedulesController_2024_06_11_getSchedule",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "scheduleId",
|
"name": "scheduleId",
|
||||||
@@ -896,7 +896,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/GetScheduleOutput"
|
"$ref": "#/components/schemas/GetScheduleOutput_2024_06_11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -907,7 +907,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"patch": {
|
"patch": {
|
||||||
"operationId": "SchedulesController_updateSchedule",
|
"operationId": "SchedulesController_2024_06_11_updateSchedule",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "scheduleId",
|
"name": "scheduleId",
|
||||||
@@ -923,7 +923,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UpdateScheduleInput"
|
"$ref": "#/components/schemas/UpdateScheduleInput_2024_06_11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -934,7 +934,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UpdateScheduleOutput"
|
"$ref": "#/components/schemas/UpdateScheduleOutput_2024_06_11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -945,7 +945,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"operationId": "SchedulesController_deleteSchedule",
|
"operationId": "SchedulesController_2024_06_11_deleteSchedule",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "scheduleId",
|
"name": "scheduleId",
|
||||||
@@ -962,7 +962,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/DeleteScheduleOutput"
|
"$ref": "#/components/schemas/DeleteScheduleOutput_2024_06_11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1214,6 +1214,113 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v2/calendars/{calendar}/connect": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "CalendarsController_redirect",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"required": true,
|
||||||
|
"in": "header",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "calendar",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Calendars"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v2/calendars/{calendar}/save": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "CalendarsController_save",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "state",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "calendar",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Calendars"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v2/calendars/{calendar}/check": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "CalendarsController_check",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "calendar",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Calendars"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/bookings": {
|
"/v2/bookings": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "BookingsController_getBookings",
|
"operationId": "BookingsController_getBookings",
|
||||||
@@ -1726,12 +1833,16 @@
|
|||||||
},
|
},
|
||||||
"refreshToken": {
|
"refreshToken": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"accessTokenExpiresAt": {
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"user",
|
"user",
|
||||||
"accessToken",
|
"accessToken",
|
||||||
"refreshToken"
|
"refreshToken",
|
||||||
|
"accessTokenExpiresAt"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"CreateManagedUserOutput": {
|
"CreateManagedUserOutput": {
|
||||||
@@ -2895,7 +3006,7 @@
|
|||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"CreateAvailabilityInput": {
|
"CreateAvailabilityInput_2024_04_15": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"days": {
|
"days": {
|
||||||
@@ -2923,7 +3034,7 @@
|
|||||||
"endTime"
|
"endTime"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"CreateScheduleInput": {
|
"CreateScheduleInput_2024_04_15": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
@@ -2935,7 +3046,7 @@
|
|||||||
"availabilities": {
|
"availabilities": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/CreateAvailabilityInput"
|
"$ref": "#/components/schemas/CreateAvailabilityInput_2024_04_15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"isDefault": {
|
"isDefault": {
|
||||||
@@ -3107,7 +3218,7 @@
|
|||||||
"readOnly"
|
"readOnly"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"CreateScheduleOutput": {
|
"CreateScheduleOutput_2024_04_15": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"status": {
|
"status": {
|
||||||
@@ -3127,7 +3238,7 @@
|
|||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"GetDefaultScheduleOutput": {
|
"GetDefaultScheduleOutput_2024_04_15": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"status": {
|
"status": {
|
||||||
@@ -3152,7 +3263,7 @@
|
|||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"GetScheduleOutput": {
|
"GetScheduleOutput_2024_04_15": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"status": {
|
"status": {
|
||||||
@@ -3172,7 +3283,7 @@
|
|||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"GetSchedulesOutput": {
|
"GetSchedulesOutput_2024_04_15": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"status": {
|
"status": {
|
||||||
@@ -3192,7 +3303,7 @@
|
|||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"UpdateScheduleInput": {
|
"UpdateScheduleInput_2024_04_15": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"timeZone": {
|
"timeZone": {
|
||||||
@@ -3252,7 +3363,7 @@
|
|||||||
"schedule"
|
"schedule"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"EventTypeModel": {
|
"EventTypeModel_2024_04_15": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -3267,7 +3378,50 @@
|
|||||||
"id"
|
"id"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ScheduleModel": {
|
"AvailabilityModel_2024_04_15": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"scheduleId": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"eventTypeId": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"days": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"startTime": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"endTime": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"days"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ScheduleModel_2024_04_15": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -3286,13 +3440,13 @@
|
|||||||
"eventType": {
|
"eventType": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/EventTypeModel"
|
"$ref": "#/components/schemas/EventTypeModel_2024_04_15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"availability": {
|
"availability": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/AvailabilityModel"
|
"$ref": "#/components/schemas/AvailabilityModel_2024_04_15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3302,11 +3456,11 @@
|
|||||||
"name"
|
"name"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"UpdatedScheduleOutput": {
|
"UpdatedScheduleOutput_2024_04_15": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"schedule": {
|
"schedule": {
|
||||||
"$ref": "#/components/schemas/ScheduleModel"
|
"$ref": "#/components/schemas/ScheduleModel_2024_04_15"
|
||||||
},
|
},
|
||||||
"isDefault": {
|
"isDefault": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@@ -3328,7 +3482,7 @@
|
|||||||
"isDefault"
|
"isDefault"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"UpdateScheduleOutput": {
|
"UpdateScheduleOutput_2024_04_15": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"status": {
|
"status": {
|
||||||
@@ -3340,7 +3494,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"$ref": "#/components/schemas/UpdatedScheduleOutput"
|
"$ref": "#/components/schemas/UpdatedScheduleOutput_2024_04_15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -3348,7 +3502,7 @@
|
|||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"DeleteScheduleOutput": {
|
"DeleteScheduleOutput_2024_04_15": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"status": {
|
"status": {
|
||||||
@@ -3454,6 +3608,349 @@
|
|||||||
"status"
|
"status"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ScheduleAvailabilityInput_2024_06_11": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"days": {
|
||||||
|
"example": [
|
||||||
|
"Monday",
|
||||||
|
"Tuesday"
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"startTime": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "09:00"
|
||||||
|
},
|
||||||
|
"endTime": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "10:00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"days",
|
||||||
|
"startTime",
|
||||||
|
"endTime"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ScheduleOverrideInput_2024_06_11": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "2024-05-20"
|
||||||
|
},
|
||||||
|
"startTime": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "12:00"
|
||||||
|
},
|
||||||
|
"endTime": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "13:00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"date",
|
||||||
|
"startTime",
|
||||||
|
"endTime"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"CreateScheduleInput_2024_06_11": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "One-on-one coaching"
|
||||||
|
},
|
||||||
|
"timeZone": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Europe/Rome"
|
||||||
|
},
|
||||||
|
"availability": {
|
||||||
|
"example": [
|
||||||
|
{
|
||||||
|
"days": [
|
||||||
|
"Monday",
|
||||||
|
"Tuesday"
|
||||||
|
],
|
||||||
|
"startTime": "09:00",
|
||||||
|
"endTime": "10:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isDefault": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"example": [
|
||||||
|
{
|
||||||
|
"date": "2024-05-20",
|
||||||
|
"startTime": "12:00",
|
||||||
|
"endTime": "14:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"timeZone",
|
||||||
|
"isDefault"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ScheduleOutput_2024_06_11": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 254
|
||||||
|
},
|
||||||
|
"ownerId": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 478
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "One-on-one coaching"
|
||||||
|
},
|
||||||
|
"timeZone": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Europe/Rome"
|
||||||
|
},
|
||||||
|
"availability": {
|
||||||
|
"example": [
|
||||||
|
{
|
||||||
|
"days": [
|
||||||
|
"Monday",
|
||||||
|
"Tuesday"
|
||||||
|
],
|
||||||
|
"startTime": "09:00",
|
||||||
|
"endTime": "10:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isDefault": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"example": [
|
||||||
|
{
|
||||||
|
"date": "2024-05-20",
|
||||||
|
"startTime": "12:00",
|
||||||
|
"endTime": "13:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"ownerId",
|
||||||
|
"name",
|
||||||
|
"timeZone",
|
||||||
|
"availability",
|
||||||
|
"isDefault",
|
||||||
|
"overrides"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"CreateScheduleOutput_2024_06_11": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "success",
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"GetDefaultScheduleOutput_2024_06_11": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "success",
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"GetScheduleOutput_2024_06_11": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "success",
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"nullable": true,
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"GetSchedulesOutput_2024_06_11": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "success",
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"UpdateScheduleInput_2024_06_11": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "One-on-one coaching"
|
||||||
|
},
|
||||||
|
"timeZone": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Europe/Rome"
|
||||||
|
},
|
||||||
|
"availability": {
|
||||||
|
"example": [
|
||||||
|
{
|
||||||
|
"days": [
|
||||||
|
"Monday",
|
||||||
|
"Tuesday"
|
||||||
|
],
|
||||||
|
"startTime": "09:00",
|
||||||
|
"endTime": "10:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isDefault": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"example": [
|
||||||
|
{
|
||||||
|
"date": "2024-05-20",
|
||||||
|
"startTime": "12:00",
|
||||||
|
"endTime": "14:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpdateScheduleOutput_2024_06_11": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "success",
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"DeleteScheduleOutput_2024_06_11": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "success",
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
},
|
||||||
"MeOutput": {
|
"MeOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ const AccountSelector: FC<AccountSelectorProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PersonalAccountProps = Pick<User, "id" | "avatar" | "name"> & { alreadyInstalled: boolean };
|
export type PersonalAccountProps = Pick<User, "id" | "avatarUrl" | "name"> & { alreadyInstalled: boolean };
|
||||||
|
|
||||||
export type TeamsProp = (Pick<Team, "id" | "name" | "logo"> & {
|
export type TeamsProp = (Pick<Team, "id" | "name" | "logoUrl"> & {
|
||||||
alreadyInstalled: boolean;
|
alreadyInstalled: boolean;
|
||||||
})[];
|
})[];
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ export const AccountsStepCard: FC<AccountStepCardProps> = ({ teams, personalAcco
|
|||||||
<div className={classNames("mt-2 flex flex-col gap-2 ")}>
|
<div className={classNames("mt-2 flex flex-col gap-2 ")}>
|
||||||
<AccountSelector
|
<AccountSelector
|
||||||
testId="install-app-button-personal"
|
testId="install-app-button-personal"
|
||||||
avatar={personalAccount.avatar ?? ""}
|
avatar={personalAccount.avatarUrl ?? ""}
|
||||||
name={personalAccount.name ?? ""}
|
name={personalAccount.name ?? ""}
|
||||||
alreadyInstalled={personalAccount.alreadyInstalled}
|
alreadyInstalled={personalAccount.alreadyInstalled}
|
||||||
onClick={() => onSelect()}
|
onClick={() => onSelect()}
|
||||||
@@ -85,7 +85,7 @@ export const AccountsStepCard: FC<AccountStepCardProps> = ({ teams, personalAcco
|
|||||||
key={team.id}
|
key={team.id}
|
||||||
testId={`install-app-button-team${team.id}`}
|
testId={`install-app-button-team${team.id}`}
|
||||||
alreadyInstalled={team.alreadyInstalled}
|
alreadyInstalled={team.alreadyInstalled}
|
||||||
avatar={team.logo ?? ""}
|
avatar={team.logoUrl ?? ""}
|
||||||
name={team.name}
|
name={team.name}
|
||||||
onClick={() => onSelect(team.id)}
|
onClick={() => onSelect(team.id)}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useForm, Controller, useFieldArray } from "react-hook-form";
|
||||||
|
|
||||||
import type { EventLocationType, getEventLocationValue } from "@calcom/app-store/locations";
|
import type { EventLocationType, getEventLocationValue } from "@calcom/app-store/locations";
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +16,7 @@ import classNames from "@calcom/lib/classNames";
|
|||||||
import { formatTime } from "@calcom/lib/date-fns";
|
import { formatTime } from "@calcom/lib/date-fns";
|
||||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
||||||
|
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { BookingStatus } from "@calcom/prisma/enums";
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
@@ -35,6 +37,14 @@ import {
|
|||||||
TableActions,
|
TableActions,
|
||||||
TextAreaField,
|
TextAreaField,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Dropdown,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
|
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
|
||||||
@@ -87,7 +97,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isUpcoming = new Date(booking.endTime) >= new Date();
|
const isUpcoming = new Date(booking.endTime) >= new Date();
|
||||||
const isPast = new Date(booking.endTime) < new Date();
|
const isBookingInPast = new Date(booking.endTime) < new Date();
|
||||||
const isCancelled = booking.status === BookingStatus.CANCELLED;
|
const isCancelled = booking.status === BookingStatus.CANCELLED;
|
||||||
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
||||||
const isRejected = booking.status === BookingStatus.REJECTED;
|
const isRejected = booking.status === BookingStatus.REJECTED;
|
||||||
@@ -220,7 +230,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPast && isPending && !isConfirmed) {
|
if (isBookingInPast && isPending && !isConfirmed) {
|
||||||
bookedActions = bookedActions.filter((action) => action.id !== "cancel");
|
bookedActions = bookedActions.filter((action) => action.id !== "cancel");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,9 +288,9 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
|
|
||||||
const title = booking.title;
|
const title = booking.title;
|
||||||
|
|
||||||
const showViewRecordingsButton = !!(booking.isRecorded && isPast && isConfirmed);
|
const showViewRecordingsButton = !!(booking.isRecorded && isBookingInPast && isConfirmed);
|
||||||
const showCheckRecordingButton =
|
const showCheckRecordingButton =
|
||||||
isPast &&
|
isBookingInPast &&
|
||||||
isConfirmed &&
|
isConfirmed &&
|
||||||
!booking.isRecorded &&
|
!booking.isRecorded &&
|
||||||
(!booking.location || booking.location === "integrations:daily" || booking?.location?.trim() === "");
|
(!booking.location || booking.location === "integrations:daily" || booking?.location?.trim() === "");
|
||||||
@@ -298,7 +308,14 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const showPendingPayment = paymentAppData.enabled && booking.payment.length && !booking.paid;
|
const showPendingPayment = paymentAppData.enabled && booking.payment.length && !booking.paid;
|
||||||
|
const attendeeList = booking.attendees.map((attendee) => {
|
||||||
|
return {
|
||||||
|
name: attendee.name,
|
||||||
|
email: attendee.email,
|
||||||
|
id: attendee.id,
|
||||||
|
noShow: attendee.noShow || false,
|
||||||
|
};
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RescheduleDialog
|
<RescheduleDialog
|
||||||
@@ -436,7 +453,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className={`w-full px-4${isRejected ? " line-through" : ""}`}>
|
<td data-testid="title-and-attendees" className={`w-full px-4${isRejected ? " line-through" : ""}`}>
|
||||||
<Link href={bookingLink}>
|
<Link href={bookingLink}>
|
||||||
{/* Time and Badges for mobile */}
|
{/* Time and Badges for mobile */}
|
||||||
<div className="w-full pb-2 pt-4 sm:hidden">
|
<div className="w-full pb-2 pt-4 sm:hidden">
|
||||||
@@ -507,9 +524,11 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
)}
|
)}
|
||||||
{booking.attendees.length !== 0 && (
|
{booking.attendees.length !== 0 && (
|
||||||
<DisplayAttendees
|
<DisplayAttendees
|
||||||
attendees={booking.attendees}
|
attendees={attendeeList}
|
||||||
user={booking.user}
|
user={booking.user}
|
||||||
currentEmail={userEmail}
|
currentEmail={userEmail}
|
||||||
|
bookingUid={booking.uid}
|
||||||
|
isBookingInPast={isBookingInPast}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isCancelled && booking.rescheduled && (
|
{isCancelled && booking.rescheduled && (
|
||||||
@@ -528,7 +547,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
{isRejected && <div className="text-subtle text-sm">{t("rejected")}</div>}
|
{isRejected && <div className="text-subtle text-sm">{t("rejected")}</div>}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
{isBookingInPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||||
{(showViewRecordingsButton || showCheckRecordingButton) && (
|
{(showViewRecordingsButton || showCheckRecordingButton) && (
|
||||||
<TableActions actions={showRecordingActions} />
|
<TableActions actions={showRecordingActions} />
|
||||||
)}
|
)}
|
||||||
@@ -654,13 +673,215 @@ const FirstAttendee = ({
|
|||||||
type AttendeeProps = {
|
type AttendeeProps = {
|
||||||
name?: string;
|
name?: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
id: number;
|
||||||
|
noShow: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Attendee = ({ email, name }: AttendeeProps) => {
|
type NoShowProps = {
|
||||||
|
bookingUid: string;
|
||||||
|
isBookingInPast: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => {
|
||||||
|
const { email, name, bookingUid, isBookingInPast, noShow: noShowAttendee } = attendeeProps;
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const [noShow, setNoShow] = useState(noShowAttendee);
|
||||||
|
const [openDropdown, setOpenDropdown] = useState(false);
|
||||||
|
const { copyToClipboard, isCopied } = useCopy();
|
||||||
|
|
||||||
|
const noShowMutation = trpc.viewer.public.noShow.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
showToast(
|
||||||
|
t(noShow ? "x_marked_as_no_show" : "x_unmarked_as_no_show", {
|
||||||
|
x: name || email,
|
||||||
|
}),
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
showToast(err.message, "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleNoShow({
|
||||||
|
attendee,
|
||||||
|
bookingUid,
|
||||||
|
}: {
|
||||||
|
attendee: { email: string; noShow: boolean };
|
||||||
|
bookingUid: string;
|
||||||
|
}) {
|
||||||
|
noShowMutation.mutate({ bookingUid, attendees: [attendee] });
|
||||||
|
setNoShow(!noShow);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a className="hover:text-blue-500" href={`mailto:${email}`} onClick={(e) => e.stopPropagation()}>
|
<Dropdown open={openDropdown} onOpenChange={setOpenDropdown}>
|
||||||
{name || email}
|
<DropdownMenuTrigger asChild>
|
||||||
</a>
|
<button
|
||||||
|
data-testid="guest"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="radix-state-open:text-blue-500 hover:text-blue-500">
|
||||||
|
{noShow ? (
|
||||||
|
<s>
|
||||||
|
{name || email} <Icon name="eye-off" className="inline h-4" />
|
||||||
|
</s>
|
||||||
|
) : (
|
||||||
|
<>{name || email}</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem className="focus:outline-none">
|
||||||
|
<DropdownItem
|
||||||
|
StartIcon="mail"
|
||||||
|
href={`mailto:${email}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
setOpenDropdown(false);
|
||||||
|
e.stopPropagation();
|
||||||
|
}}>
|
||||||
|
<a href={`mailto:${email}`}>{t("email")}</a>
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem className="focus:outline-none">
|
||||||
|
<DropdownItem
|
||||||
|
StartIcon={isCopied ? "clipboard-check" : "clipboard"}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
copyToClipboard(email);
|
||||||
|
setOpenDropdown(false);
|
||||||
|
showToast(t("email_copied"), "success");
|
||||||
|
}}>
|
||||||
|
{!isCopied ? t("copy") : t("copied")}
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{isBookingInPast && (
|
||||||
|
<DropdownMenuItem className="focus:outline-none">
|
||||||
|
{noShow ? (
|
||||||
|
<DropdownItem
|
||||||
|
data-testid="unmark-no-show"
|
||||||
|
onClick={(e) => {
|
||||||
|
setOpenDropdown(false);
|
||||||
|
toggleNoShow({ attendee: { noShow: false, email }, bookingUid });
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
StartIcon="eye">
|
||||||
|
{t("unmark_as_no_show")}
|
||||||
|
</DropdownItem>
|
||||||
|
) : (
|
||||||
|
<DropdownItem
|
||||||
|
data-testid="mark-no-show"
|
||||||
|
onClick={(e) => {
|
||||||
|
setOpenDropdown(false);
|
||||||
|
toggleNoShow({ attendee: { noShow: true, email }, bookingUid });
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
StartIcon="eye-off">
|
||||||
|
{t("mark_as_no_show")}
|
||||||
|
</DropdownItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type GroupedAttendeeProps = {
|
||||||
|
attendees: AttendeeProps[];
|
||||||
|
bookingUid: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => {
|
||||||
|
const { bookingUid } = groupedAttendeeProps;
|
||||||
|
const attendees = groupedAttendeeProps.attendees.map((attendee) => {
|
||||||
|
return {
|
||||||
|
id: attendee.id,
|
||||||
|
email: attendee.email,
|
||||||
|
name: attendee.name,
|
||||||
|
noShow: attendee.noShow || false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const { t } = useLocale();
|
||||||
|
const noShowMutation = trpc.viewer.public.noShow.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
showToast(t("no_show_updated"), "success");
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
showToast(err.message, "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { control, handleSubmit } = useForm<{
|
||||||
|
attendees: AttendeeProps[];
|
||||||
|
}>({
|
||||||
|
defaultValues: {
|
||||||
|
attendees,
|
||||||
|
},
|
||||||
|
mode: "onBlur",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { fields } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "attendees",
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data: { attendees: AttendeeProps[] }) => {
|
||||||
|
const filteredData = data.attendees.slice(1);
|
||||||
|
noShowMutation.mutate({ bookingUid, attendees: filteredData });
|
||||||
|
setOpenDropdown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [openDropdown, setOpenDropdown] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown open={openDropdown} onOpenChange={setOpenDropdown}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
data-testid="more-guests"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="radix-state-open:text-blue-500 hover:text-blue-500 focus:outline-none">
|
||||||
|
{t("plus_more", { count: attendees.length - 1 })}
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuLabel className="text-xs font-medium uppercase">
|
||||||
|
{t("mark_as_no_show_title")}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
{fields.slice(1).map((field, index) => (
|
||||||
|
<Controller
|
||||||
|
key={field.id}
|
||||||
|
name={`attendees.${index + 1}.noShow`}
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
checked={value || false}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
className="pr-8 focus:outline-none"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onChange(!value);
|
||||||
|
}}>
|
||||||
|
<span className={value ? "line-through" : ""}>{field.email}</span>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<div className=" flex justify-end p-2">
|
||||||
|
<Button
|
||||||
|
data-testid="update-no-show"
|
||||||
|
color="secondary"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit(onSubmit)();
|
||||||
|
}}>
|
||||||
|
{t("mark_as_no_show_title")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -668,17 +889,23 @@ const DisplayAttendees = ({
|
|||||||
attendees,
|
attendees,
|
||||||
user,
|
user,
|
||||||
currentEmail,
|
currentEmail,
|
||||||
|
bookingUid,
|
||||||
|
isBookingInPast,
|
||||||
}: {
|
}: {
|
||||||
attendees: AttendeeProps[];
|
attendees: AttendeeProps[];
|
||||||
user: UserProps | null;
|
user: UserProps | null;
|
||||||
currentEmail?: string | null;
|
currentEmail?: string | null;
|
||||||
|
bookingUid: string;
|
||||||
|
isBookingInPast: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
attendees.sort((a, b) => a.id - b.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-emphasis text-sm">
|
<div className="text-emphasis text-sm">
|
||||||
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
||||||
{attendees.length > 1 ? <span>, </span> : <span> {t("and")} </span>}
|
{attendees.length > 1 ? <span>, </span> : <span> {t("and")} </span>}
|
||||||
<Attendee {...attendees[0]} />
|
<Attendee {...attendees[0]} bookingUid={bookingUid} isBookingInPast={isBookingInPast} />
|
||||||
{attendees.length > 1 && (
|
{attendees.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<div className="text-emphasis inline-block text-sm"> {t("and")} </div>
|
<div className="text-emphasis inline-block text-sm"> {t("and")} </div>
|
||||||
@@ -686,13 +913,13 @@ const DisplayAttendees = ({
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
content={attendees.slice(1).map((attendee) => (
|
content={attendees.slice(1).map((attendee) => (
|
||||||
<p key={attendee.email}>
|
<p key={attendee.email}>
|
||||||
<Attendee {...attendee} />
|
<Attendee {...attendee} bookingUid={bookingUid} isBookingInPast={isBookingInPast} />
|
||||||
</p>
|
</p>
|
||||||
))}>
|
))}>
|
||||||
<div className="inline-block">{t("plus_more", { count: attendees.length - 1 })}</div>
|
{isBookingInPast && <GroupedAttendees attendees={attendees} bookingUid={bookingUid} />}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Attendee {...attendees[1]} />
|
<Attendee {...attendees[1]} bookingUid={bookingUid} isBookingInPast={isBookingInPast} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import type { MutableRefObject } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Dialog, DialogContent, Button, DialogFooter } from "@calcom/ui";
|
||||||
|
|
||||||
|
interface AssignmentWarningDialogProps {
|
||||||
|
isOpenAssignmentWarnDialog: boolean;
|
||||||
|
setIsOpenAssignmentWarnDialog: Dispatch<SetStateAction<boolean>>;
|
||||||
|
pendingRoute: string;
|
||||||
|
leaveWithoutAssigningHosts: MutableRefObject<boolean>;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
const AssignmentWarningDialog = (props: AssignmentWarningDialogProps) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const {
|
||||||
|
isOpenAssignmentWarnDialog,
|
||||||
|
setIsOpenAssignmentWarnDialog,
|
||||||
|
pendingRoute,
|
||||||
|
leaveWithoutAssigningHosts,
|
||||||
|
id,
|
||||||
|
} = props;
|
||||||
|
const router = useRouter();
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpenAssignmentWarnDialog} onOpenChange={setIsOpenAssignmentWarnDialog}>
|
||||||
|
<DialogContent title={t("leave_without_assigning_anyone")} Icon="circle-alert" type="confirmation">
|
||||||
|
<div className="text-subtle text-sm">
|
||||||
|
<p className="mt-3">{t("leave_without_adding_attendees")}</p>
|
||||||
|
<p className="mt-4">{t("no_availability_shown_to_bookers")}</p>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="mt-6">
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsOpenAssignmentWarnDialog(false);
|
||||||
|
router.replace(`/event-types/${id}?tabName=team`);
|
||||||
|
}}
|
||||||
|
color="minimal">
|
||||||
|
{t("go_back_and_assign")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsOpenAssignmentWarnDialog(false);
|
||||||
|
leaveWithoutAssigningHosts.current = true;
|
||||||
|
router.replace(pendingRoute);
|
||||||
|
}}>
|
||||||
|
{t("leave_without_assigning")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default AssignmentWarningDialog;
|
||||||
@@ -82,6 +82,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
);
|
);
|
||||||
const placeholderHashedLink = `${WEBSITE_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
|
const placeholderHashedLink = `${WEBSITE_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
|
||||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||||
|
const multiLocation = (formMethods.getValues("locations") || []).length > 1;
|
||||||
const noShowFeeEnabled =
|
const noShowFeeEnabled =
|
||||||
formMethods.getValues("metadata")?.apps?.stripe?.enabled === true &&
|
formMethods.getValues("metadata")?.apps?.stripe?.enabled === true &&
|
||||||
formMethods.getValues("metadata")?.apps?.stripe?.paymentOption === "HOLD";
|
formMethods.getValues("metadata")?.apps?.stripe?.paymentOption === "HOLD";
|
||||||
@@ -434,7 +435,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
{...seatsLocked}
|
{...seatsLocked}
|
||||||
description={t("offer_seats_description")}
|
description={t("offer_seats_description")}
|
||||||
checked={value}
|
checked={value}
|
||||||
disabled={noShowFeeEnabled}
|
disabled={noShowFeeEnabled || multiLocation}
|
||||||
|
tooltip={
|
||||||
|
multiLocation
|
||||||
|
? t("multilocation_doesnt_support_seats")
|
||||||
|
: noShowFeeEnabled
|
||||||
|
? t("no_show_fee_doesnt_support_seats")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||||
if (e) {
|
if (e) {
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export const EventSetupTab = (
|
|||||||
|
|
||||||
const [animationRef] = useAutoAnimate<HTMLUListElement>();
|
const [animationRef] = useAutoAnimate<HTMLUListElement>();
|
||||||
|
|
||||||
|
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
||||||
const validLocations = formMethods.getValues("locations").filter((location) => {
|
const validLocations = formMethods.getValues("locations").filter((location) => {
|
||||||
const eventLocation = getEventLocationType(location.type);
|
const eventLocation = getEventLocationType(location.type);
|
||||||
if (!eventLocation) {
|
if (!eventLocation) {
|
||||||
@@ -444,6 +445,8 @@ export const EventSetupTab = (
|
|||||||
data-testid="add-location"
|
data-testid="add-location"
|
||||||
StartIcon="plus"
|
StartIcon="plus"
|
||||||
color="minimal"
|
color="minimal"
|
||||||
|
disabled={seatsEnabled}
|
||||||
|
tooltip={seatsEnabled ? t("seats_option_doesnt_support_multi_location") : undefined}
|
||||||
onClick={() => setShowEmptyLocationSelect(true)}>
|
onClick={() => setShowEmptyLocationSelect(true)}>
|
||||||
{t("add_location")}
|
{t("add_location")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export const mapMemberToChildrenOption = (
|
|||||||
membership: member.membership,
|
membership: member.membership,
|
||||||
eventTypeSlugs: member.eventTypes ?? [],
|
eventTypeSlugs: member.eventTypes ?? [],
|
||||||
avatar: member.avatar,
|
avatar: member.avatar,
|
||||||
|
profile: member.profile,
|
||||||
},
|
},
|
||||||
value: `${member.id ?? ""}`,
|
value: `${member.id ?? ""}`,
|
||||||
label: `${member.name || member.email || ""}${!member.username ? ` (${pendingString})` : ""}`,
|
label: `${member.name || member.email || ""}${!member.username ? ` (${pendingString})` : ""}`,
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { EmptyScreen, Button } from "@calcom/ui";
|
||||||
|
|
||||||
|
export default function NoPlatformPlan() {
|
||||||
|
return (
|
||||||
|
<EmptyScreen
|
||||||
|
Icon="credit-card"
|
||||||
|
headline="Subscription needed"
|
||||||
|
description="You are not subscribed to a Platform plan."
|
||||||
|
buttonRaw={
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button href="https://cal.com/platform/pricing">Go to Pricing</Button>
|
||||||
|
<Button color="secondary" href="https://cal.com/pricing">
|
||||||
|
Contact Sales
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
export function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
// reduce sample rate to 10% on production
|
||||||
|
tracesSampleRate: process.env.NODE_ENV !== "production" ? 1.0 : 0.1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NEXT_RUNTIME === "edge") {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { getServerSession } from "@calcom/feature-auth/lib/getServerSession";
|
||||||
|
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const session = await getServerSession({
|
||||||
|
req: context.req,
|
||||||
|
res: context.res,
|
||||||
|
authOptions: AUTH_OPTIONS,
|
||||||
|
});
|
||||||
|
// Disable this check if we ever make this self serve.
|
||||||
|
if (session?.user.role !== "ADMIN") {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -955,6 +955,7 @@ const EventTypesPage: React.FC & {
|
|||||||
const orgBranding = useOrgBranding();
|
const orgBranding = useOrgBranding();
|
||||||
const routerQuery = useRouterQuery();
|
const routerQuery = useRouterQuery();
|
||||||
const filters = getTeamsFiltersFromQuery(routerQuery);
|
const filters = getTeamsFiltersFromQuery(routerQuery);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
// TODO: Maybe useSuspenseQuery to focus on success case only? Remember that it would crash the page when there is an error in query. Also, it won't support skeleton
|
// TODO: Maybe useSuspenseQuery to focus on success case only? Remember that it would crash the page when there is an error in query. Also, it won't support skeleton
|
||||||
const { data, status, error } = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters }, {
|
const { data, status, error } = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters }, {
|
||||||
@@ -967,6 +968,13 @@ const EventTypesPage: React.FC & {
|
|||||||
if (searchParams?.get("openIntercom") === "true") {
|
if (searchParams?.get("openIntercom") === "true") {
|
||||||
open();
|
open();
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* During signup, if the account already exists, we redirect the user to /event-types instead of onboarding.
|
||||||
|
* Adding this redirection logic here as well to ensure the user is redirected to the correct redirectUrl.
|
||||||
|
*/
|
||||||
|
const redirectUrl = localStorage.getItem("onBoardingRedirect");
|
||||||
|
localStorage.removeItem("onBoardingRedirect");
|
||||||
|
redirectUrl && router.push(redirectUrl);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useMemo, useState, useRef } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -113,6 +115,8 @@ const EventAITab = dynamic(() => import("@components/eventtype/EventAITab").then
|
|||||||
|
|
||||||
const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/ManagedEventDialog"));
|
const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/ManagedEventDialog"));
|
||||||
|
|
||||||
|
const AssignmentWarningDialog = dynamic(() => import("@components/eventtype/AssignmentWarningDialog"));
|
||||||
|
|
||||||
export type Host = { isFixed: boolean; userId: number; priority: number };
|
export type Host = { isFixed: boolean; userId: number; priority: number };
|
||||||
|
|
||||||
export type CustomInputParsed = typeof customInputSchema._output;
|
export type CustomInputParsed = typeof customInputSchema._output;
|
||||||
@@ -154,6 +158,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
|
const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
|
||||||
|
const [isOpenAssignmentWarnDialog, setIsOpenAssignmentWarnDialog] = useState<boolean>(false);
|
||||||
|
const [pendingRoute, setPendingRoute] = useState("");
|
||||||
|
const leaveWithoutAssigningHosts = useRef(false);
|
||||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
@@ -196,6 +203,32 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleRouteChange = (url: string) => {
|
||||||
|
const paths = url.split("/");
|
||||||
|
if (
|
||||||
|
!!team &&
|
||||||
|
eventType.hosts.length === 0 &&
|
||||||
|
!leaveWithoutAssigningHosts.current &&
|
||||||
|
(url === "/event-types" || paths[1] !== "event-types")
|
||||||
|
) {
|
||||||
|
setIsOpenAssignmentWarnDialog(true);
|
||||||
|
setPendingRoute(url);
|
||||||
|
router.events.emit(
|
||||||
|
"routeChangeError",
|
||||||
|
new Error(`Aborted route change to ${url} because none was assigned to team event`)
|
||||||
|
);
|
||||||
|
throw "Aborted";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
router.events.on("routeChangeStart", handleRouteChange);
|
||||||
|
return () => {
|
||||||
|
router.events.off("routeChangeStart", handleRouteChange);
|
||||||
|
};
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
|
const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
|
||||||
startDate: new Date(eventType.periodStartDate || Date.now()),
|
startDate: new Date(eventType.periodStartDate || Date.now()),
|
||||||
endDate: new Date(eventType.periodEndDate || Date.now()),
|
endDate: new Date(eventType.periodEndDate || Date.now()),
|
||||||
@@ -774,7 +807,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||||
</Form>
|
</Form>
|
||||||
</EventTypeSingleLayout>
|
</EventTypeSingleLayout>
|
||||||
|
|
||||||
{slugExistsChildrenDialogOpen.length ? (
|
{slugExistsChildrenDialogOpen.length ? (
|
||||||
<ManagedEventTypeDialog
|
<ManagedEventTypeDialog
|
||||||
slugExistsChildrenDialogOpen={slugExistsChildrenDialogOpen}
|
slugExistsChildrenDialogOpen={slugExistsChildrenDialogOpen}
|
||||||
@@ -791,10 +823,16 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
<AssignmentWarningDialog
|
||||||
|
isOpenAssignmentWarnDialog={isOpenAssignmentWarnDialog}
|
||||||
|
setIsOpenAssignmentWarnDialog={setIsOpenAssignmentWarnDialog}
|
||||||
|
pendingRoute={pendingRoute}
|
||||||
|
leaveWithoutAssigningHosts={leaveWithoutAssigningHosts}
|
||||||
|
id={eventType.id}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const EventTypePageWrapper: React.FC<PageProps> & {
|
const EventTypePageWrapper: React.FC<PageProps> & {
|
||||||
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||||
getLayout?: AppProps["Component"]["getLayout"];
|
getLayout?: AppProps["Component"]["getLayout"];
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||||
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { HeadSeo, Icon, UnpublishedEntity, UserAvatar } from "@calcom/ui";
|
import { HeadSeo, Icon, UnpublishedEntity, UserAvatar } from "@calcom/ui";
|
||||||
@@ -26,7 +25,6 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
|||||||
|
|
||||||
const [user] = users; //To be used when we only have a single user, not dynamic group
|
const [user] = users; //To be used when we only have a single user, not dynamic group
|
||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
const isBioEmpty = !user.bio || !user.bio.replace("<p><br></p>", "").length;
|
const isBioEmpty = !user.bio || !user.bio.replace("<p><br></p>", "").length;
|
||||||
|
|
||||||
@@ -52,7 +50,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
|||||||
}, [telemetry, router.asPath]); */
|
}, [telemetry, router.asPath]); */
|
||||||
if (entity.considerUnpublished) {
|
if (entity.considerUnpublished) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-[100dvh] items-center justify-center">
|
<div className="flex h-full min-h-[calc(100dvh)] items-center justify-center">
|
||||||
<UnpublishedEntity {...entity} />
|
<UnpublishedEntity {...entity} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -81,7 +79,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
|||||||
<main
|
<main
|
||||||
className={classNames(
|
className={classNames(
|
||||||
shouldAlignCentrally ? "mx-auto" : "",
|
shouldAlignCentrally ? "mx-auto" : "",
|
||||||
isEmbed ? "border-booker border-booker-width bg-default rounded-md border" : "",
|
isEmbed ? "border-booker border-booker-width bg-default rounded-md" : "",
|
||||||
"max-w-3xl px-4 py-24"
|
"max-w-3xl px-4 py-24"
|
||||||
)}>
|
)}>
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
@@ -126,7 +124,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
|||||||
<div
|
<div
|
||||||
key={type.id}
|
key={type.id}
|
||||||
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
||||||
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b transition first:rounded-t-md last:rounded-b-md last:border-b-0">
|
||||||
<Icon
|
<Icon
|
||||||
name="arrow-right"
|
name="arrow-right"
|
||||||
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
|
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
|
|||||||
+10
-8
@@ -158,10 +158,12 @@ const matcherConfigUserTypeEmbedRoute = {
|
|||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
output: "standalone",
|
||||||
experimental: {
|
experimental: {
|
||||||
// externalize server-side node_modules with size > 1mb, to improve dev mode performance/RAM usage
|
// externalize server-side node_modules with size > 1mb, to improve dev mode performance/RAM usage
|
||||||
serverComponentsExternalPackages: ["next-i18next"],
|
serverComponentsExternalPackages: ["next-i18next"],
|
||||||
optimizePackageImports: ["@calcom/ui"],
|
optimizePackageImports: ["@calcom/ui"],
|
||||||
|
instrumentationHook: true,
|
||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
...i18n,
|
...i18n,
|
||||||
@@ -590,14 +592,14 @@ const nextConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||||
nextConfig["sentry"] = {
|
plugins.push((nextConfig) =>
|
||||||
autoInstrumentServerFunctions: true,
|
withSentryConfig(nextConfig, {
|
||||||
hideSourceMaps: true,
|
autoInstrumentServerFunctions: true,
|
||||||
// disable source map generation for the server code
|
hideSourceMaps: true,
|
||||||
disableServerWebpackPlugin: !!process.env.SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN,
|
// disable source map generation for the server code
|
||||||
};
|
disableServerWebpackPlugin: !!process.env.SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN,
|
||||||
|
})
|
||||||
plugins.push(withSentryConfig);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "4.1.7",
|
"version": "4.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"@radix-ui/react-switch": "^1.0.0",
|
"@radix-ui/react-switch": "^1.0.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.0.0",
|
"@radix-ui/react-toggle-group": "^1.0.0",
|
||||||
"@radix-ui/react-tooltip": "^1.0.0",
|
"@radix-ui/react-tooltip": "^1.0.0",
|
||||||
"@sentry/nextjs": "^7.73.0",
|
"@sentry/nextjs": "^8.8.0",
|
||||||
"@stripe/react-stripe-js": "^1.10.0",
|
"@stripe/react-stripe-js": "^1.10.0",
|
||||||
"@stripe/stripe-js": "^1.35.0",
|
"@stripe/stripe-js": "^1.35.0",
|
||||||
"@tanstack/react-query": "^5.17.15",
|
"@tanstack/react-query": "^5.17.15",
|
||||||
|
|||||||
@@ -3,10 +3,16 @@ import { createHmac } from "crypto";
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
import {
|
||||||
|
getDownloadLinkOfCalVideoByRecordingId,
|
||||||
|
submitBatchProcessorTranscriptionJob,
|
||||||
|
} from "@calcom/core/videoClient";
|
||||||
|
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient";
|
||||||
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
|
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
|
||||||
|
import { sendDailyVideoTranscriptEmails } from "@calcom/emails";
|
||||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||||
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||||
|
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
||||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
@@ -17,26 +23,40 @@ import type { CalendarEvent } from "@calcom/types/Calendar";
|
|||||||
|
|
||||||
const log = logger.getSubLogger({ prefix: ["daily-video-webhook-handler"] });
|
const log = logger.getSubLogger({ prefix: ["daily-video-webhook-handler"] });
|
||||||
|
|
||||||
const schema = z
|
const commonSchema = z
|
||||||
.object({
|
.object({
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
payload: z.object({
|
|
||||||
recording_id: z.string(),
|
|
||||||
end_ts: z.number().optional(),
|
|
||||||
room_name: z.string(),
|
|
||||||
start_ts: z.number().optional(),
|
|
||||||
status: z.string(),
|
|
||||||
|
|
||||||
max_participants: z.number().optional(),
|
|
||||||
duration: z.number().optional(),
|
|
||||||
s3_key: z.string().optional(),
|
|
||||||
}),
|
|
||||||
event_ts: z.number().optional(),
|
event_ts: z.number().optional(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|
||||||
|
const meetingEndedSchema = commonSchema.extend({
|
||||||
|
payload: z
|
||||||
|
.object({
|
||||||
|
meeting_id: z.string(),
|
||||||
|
end_ts: z.number().optional(),
|
||||||
|
room: z.string(),
|
||||||
|
start_ts: z.number().optional(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const recordingReadySchema = commonSchema.extend({
|
||||||
|
payload: z.object({
|
||||||
|
recording_id: z.string(),
|
||||||
|
end_ts: z.number().optional(),
|
||||||
|
room_name: z.string(),
|
||||||
|
start_ts: z.number().optional(),
|
||||||
|
status: z.string(),
|
||||||
|
|
||||||
|
max_participants: z.number().optional(),
|
||||||
|
duration: z.number().optional(),
|
||||||
|
s3_key: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const downloadLinkSchema = z.object({
|
const downloadLinkSchema = z.object({
|
||||||
download_link: z.string(),
|
download_link: z.string(),
|
||||||
});
|
});
|
||||||
@@ -60,11 +80,16 @@ const triggerWebhook = async ({
|
|||||||
// Send Webhook call if hooked to BOOKING.RECORDING_READY
|
// Send Webhook call if hooked to BOOKING.RECORDING_READY
|
||||||
const triggerForUser = !booking.teamId || (booking.teamId && booking.eventTypeParentId);
|
const triggerForUser = !booking.teamId || (booking.teamId && booking.eventTypeParentId);
|
||||||
|
|
||||||
|
const organizerUserId = triggerForUser ? booking.userId : null;
|
||||||
|
|
||||||
|
const orgId = await getOrgIdFromMemberOrTeamId({ memberId: organizerUserId, teamId: booking.teamId });
|
||||||
|
|
||||||
const subscriberOptions = {
|
const subscriberOptions = {
|
||||||
userId: triggerForUser ? booking.userId : null,
|
userId: organizerUserId,
|
||||||
eventTypeId: booking.eventTypeId,
|
eventTypeId: booking.eventTypeId,
|
||||||
triggerEvent: eventTrigger,
|
triggerEvent: eventTrigger,
|
||||||
teamId: booking.teamId,
|
teamId: booking.teamId,
|
||||||
|
orgId,
|
||||||
};
|
};
|
||||||
const webhooks = await getWebhooks(subscriberOptions);
|
const webhooks = await getWebhooks(subscriberOptions);
|
||||||
|
|
||||||
@@ -89,6 +114,102 @@ const triggerWebhook = async ({
|
|||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBooking = async (bookingId: number) => {
|
||||||
|
const booking = await prisma.booking.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
id: bookingId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
...bookingMinimalSelect,
|
||||||
|
uid: true,
|
||||||
|
location: true,
|
||||||
|
isRecorded: true,
|
||||||
|
eventTypeId: true,
|
||||||
|
eventType: {
|
||||||
|
select: {
|
||||||
|
teamId: true,
|
||||||
|
parentId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
timeZone: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
locale: true,
|
||||||
|
destinationCalendar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
log.error(
|
||||||
|
"Couldn't find Booking Id:",
|
||||||
|
safeStringify({
|
||||||
|
bookingId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return booking;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBookingReference = async (roomName: string) => {
|
||||||
|
const bookingReference = await prisma.bookingReference.findFirst({
|
||||||
|
where: { type: "daily_video", uid: roomName, meetingId: roomName },
|
||||||
|
select: { bookingId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bookingReference) {
|
||||||
|
log.error(
|
||||||
|
"bookingReference not found error:",
|
||||||
|
safeStringify({
|
||||||
|
bookingReference,
|
||||||
|
roomName,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookingReference;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCalendarEvent = async (booking: Awaited<ReturnType<typeof getBooking>>) => {
|
||||||
|
const t = await getTranslation(booking?.user?.locale ?? "en", "common");
|
||||||
|
|
||||||
|
const attendeesListPromises = booking.attendees.map(async (attendee) => {
|
||||||
|
return {
|
||||||
|
id: attendee.id,
|
||||||
|
name: attendee.name,
|
||||||
|
email: attendee.email,
|
||||||
|
timeZone: attendee.timeZone,
|
||||||
|
language: {
|
||||||
|
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
||||||
|
locale: attendee.locale ?? "en",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const attendeesList = await Promise.all(attendeesListPromises);
|
||||||
|
const evt: CalendarEvent = {
|
||||||
|
type: booking.title,
|
||||||
|
title: booking.title,
|
||||||
|
description: booking.description || undefined,
|
||||||
|
startTime: booking.startTime.toISOString(),
|
||||||
|
endTime: booking.endTime.toISOString(),
|
||||||
|
organizer: {
|
||||||
|
email: booking?.userPrimaryEmail || booking.user?.email || "Email-less",
|
||||||
|
name: booking.user?.name || "Nameless",
|
||||||
|
timeZone: booking.user?.timeZone || "Europe/London",
|
||||||
|
language: { translate: t, locale: booking?.user?.locale ?? "en" },
|
||||||
|
},
|
||||||
|
attendees: attendeesList,
|
||||||
|
uid: booking.uid,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Promise.resolve(evt);
|
||||||
|
};
|
||||||
|
|
||||||
const testRequestSchema = z.object({
|
const testRequestSchema = z.object({
|
||||||
test: z.enum(["test"]),
|
test: z.enum(["test"]),
|
||||||
});
|
});
|
||||||
@@ -116,157 +237,121 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
return res.status(403).json({ message: "Signature does not match" });
|
return res.status(403).json({ message: "Signature does not match" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = schema.safeParse(req.body);
|
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
"Daily video recording webhook Request Body:",
|
"Daily video webhook Request Body:",
|
||||||
safeStringify({
|
safeStringify({
|
||||||
response,
|
body: req.body,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.success || response.data.type !== "recording.ready-to-download") {
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Invalid Payload",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { room_name, recording_id, status } = response.data.payload;
|
|
||||||
|
|
||||||
if (status !== "finished") {
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Recording not finished",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const bookingReference = await prisma.bookingReference.findFirst({
|
if (req.body?.type === "recording.ready-to-download") {
|
||||||
where: { type: "daily_video", uid: room_name, meetingId: room_name },
|
const recordingReadyResponse = recordingReadySchema.safeParse(req.body);
|
||||||
select: { bookingId: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!bookingReference || !bookingReference.bookingId) {
|
if (!recordingReadyResponse.success) {
|
||||||
log.error(
|
return res.status(400).send({
|
||||||
"bookingReference not found error:",
|
message: "Invalid Payload",
|
||||||
safeStringify({
|
});
|
||||||
bookingReference,
|
}
|
||||||
room_name,
|
|
||||||
recording_id,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return res.status(200).send({ message: "Booking reference not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const booking = await prisma.booking.findUniqueOrThrow({
|
const { room_name, recording_id, status } = recordingReadyResponse.data.payload;
|
||||||
where: {
|
|
||||||
id: bookingReference.bookingId,
|
if (status !== "finished") {
|
||||||
},
|
return res.status(400).send({
|
||||||
select: {
|
message: "Recording not finished",
|
||||||
...bookingMinimalSelect,
|
});
|
||||||
uid: true,
|
}
|
||||||
location: true,
|
const bookingReference = await getBookingReference(room_name);
|
||||||
isRecorded: true,
|
|
||||||
eventTypeId: true,
|
if (!bookingReference || !bookingReference.bookingId) {
|
||||||
eventType: {
|
return res.status(200).send({ message: "Booking reference not found" });
|
||||||
select: {
|
}
|
||||||
teamId: true,
|
|
||||||
parentId: true,
|
const booking = await getBooking(bookingReference.bookingId);
|
||||||
},
|
|
||||||
|
if (!booking) {
|
||||||
|
return res.status(404).send({
|
||||||
|
message: `Booking of room_name ${room_name} does not exist or does not contain daily video as location`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const evt = await getCalendarEvent(booking);
|
||||||
|
|
||||||
|
await prisma.booking.update({
|
||||||
|
where: {
|
||||||
|
uid: booking.uid,
|
||||||
},
|
},
|
||||||
user: {
|
data: {
|
||||||
select: {
|
isRecorded: true,
|
||||||
id: true,
|
|
||||||
timeZone: true,
|
|
||||||
email: true,
|
|
||||||
name: true,
|
|
||||||
locale: true,
|
|
||||||
destinationCalendar: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!booking) {
|
|
||||||
log.error(
|
|
||||||
"Booking:",
|
|
||||||
safeStringify({
|
|
||||||
booking,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(404).send({
|
|
||||||
message: `Booking of room_name ${room_name} does not exist or does not contain daily video as location`,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const t = await getTranslation(booking?.user?.locale ?? "en", "common");
|
const response = await getDownloadLinkOfCalVideoByRecordingId(recording_id);
|
||||||
const attendeesListPromises = booking.attendees.map(async (attendee) => {
|
const downloadLinkResponse = downloadLinkSchema.parse(response);
|
||||||
return {
|
const downloadLink = downloadLinkResponse.download_link;
|
||||||
id: attendee.id,
|
|
||||||
name: attendee.name,
|
const teamId = await getTeamIdFromEventType({
|
||||||
email: attendee.email,
|
eventType: {
|
||||||
timeZone: attendee.timeZone,
|
team: { id: booking?.eventType?.teamId ?? null },
|
||||||
language: {
|
parentId: booking?.eventType?.parentId ?? null,
|
||||||
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
|
||||||
locale: attendee.locale ?? "en",
|
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const attendeesList = await Promise.all(attendeesListPromises);
|
await triggerWebhook({
|
||||||
|
evt,
|
||||||
|
downloadLink,
|
||||||
|
booking: {
|
||||||
|
userId: booking?.user?.id,
|
||||||
|
eventTypeId: booking.eventTypeId,
|
||||||
|
eventTypeParentId: booking.eventType?.parentId,
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await prisma.booking.update({
|
try {
|
||||||
where: {
|
// Submit Transcription Batch Processor Job
|
||||||
uid: booking.uid,
|
await submitBatchProcessorTranscriptionJob(recording_id);
|
||||||
},
|
} catch (err) {
|
||||||
data: {
|
log.error("Failed to Submit Transcription Batch Processor Job:", safeStringify(err));
|
||||||
isRecorded: true,
|
}
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await getDownloadLinkOfCalVideoByRecordingId(recording_id);
|
// send emails to all attendees only when user has team plan
|
||||||
const downloadLinkResponse = downloadLinkSchema.parse(response);
|
await sendDailyVideoRecordingEmails(evt, downloadLink);
|
||||||
const downloadLink = downloadLinkResponse.download_link;
|
return res.status(200).json({ message: "Success" });
|
||||||
|
} else if (req.body.type === "meeting.ended") {
|
||||||
|
const meetingEndedResponse = meetingEndedSchema.safeParse(req.body);
|
||||||
|
if (!meetingEndedResponse.success) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Invalid Payload",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { room } = meetingEndedResponse.data.payload;
|
||||||
|
|
||||||
const evt: CalendarEvent = {
|
const bookingReference = await getBookingReference(room);
|
||||||
type: booking.title,
|
if (!bookingReference || !bookingReference.bookingId) {
|
||||||
title: booking.title,
|
return res.status(200).send({ message: "Booking reference not found" });
|
||||||
description: booking.description || undefined,
|
}
|
||||||
startTime: booking.startTime.toISOString(),
|
|
||||||
endTime: booking.endTime.toISOString(),
|
|
||||||
organizer: {
|
|
||||||
email: booking?.userPrimaryEmail || booking.user?.email || "Email-less",
|
|
||||||
name: booking.user?.name || "Nameless",
|
|
||||||
timeZone: booking.user?.timeZone || "Europe/London",
|
|
||||||
language: { translate: t, locale: booking?.user?.locale ?? "en" },
|
|
||||||
},
|
|
||||||
attendees: attendeesList,
|
|
||||||
uid: booking.uid,
|
|
||||||
};
|
|
||||||
|
|
||||||
const teamId = await getTeamIdFromEventType({
|
const booking = await getBooking(bookingReference.bookingId);
|
||||||
eventType: {
|
|
||||||
team: { id: booking?.eventType?.teamId ?? null },
|
|
||||||
parentId: booking?.eventType?.parentId ?? null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await triggerWebhook({
|
if (!booking) {
|
||||||
evt,
|
return res.status(404).send({
|
||||||
downloadLink,
|
message: `Booking of room_name ${room} does not exist or does not contain daily video as location`,
|
||||||
booking: {
|
});
|
||||||
userId: booking?.user?.id,
|
}
|
||||||
eventTypeId: booking.eventTypeId,
|
|
||||||
eventTypeParentId: booking.eventType?.parentId,
|
|
||||||
teamId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// send emails to all attendees only when user has team plan
|
const transcripts = await getAllTranscriptsAccessLinkFromRoomName(room);
|
||||||
await sendDailyVideoRecordingEmails(evt, downloadLink);
|
|
||||||
return res.status(200).json({ message: "Success" });
|
if (!transcripts || !transcripts.length)
|
||||||
|
return res.status(200).json({ message: `No Transcripts found for room name ${room}` });
|
||||||
|
|
||||||
|
const evt = await getCalendarEvent(booking);
|
||||||
|
await sendDailyVideoTranscriptEmails(evt, transcripts);
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Success" });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error in /recorded-daily-video", err);
|
log.error("Error in /recorded-daily-video", err);
|
||||||
return res.status(500).json({ message: "something went wrong" });
|
return res.status(500).json({ message: "something went wrong" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loaded 100 of 330 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user