Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60ae817f30 | ||
|
|
74166fc2cb | ||
|
|
fb997045f2 | ||
|
|
73a79cf29b | ||
|
|
54a1663a8b | ||
|
|
883d745784 | ||
|
|
571e93a19a | ||
|
|
d16428da6c | ||
|
|
8c5438663c | ||
|
|
ed76d163cf | ||
|
|
9215725dad | ||
|
|
00131668aa | ||
|
|
0306ad737f | ||
|
|
20dfc34e70 | ||
|
|
b7e8b252d2 | ||
|
|
74f09c332b | ||
|
|
3ce6c23603 | ||
|
|
ad3ee14501 | ||
|
|
1931758fc7 | ||
|
|
fe551af145 | ||
|
|
9fdaaed100 | ||
|
|
c0f8a355e3 | ||
|
|
df95ec4ce8 | ||
|
|
a15cad4d03 | ||
|
|
8904f20eb8 | ||
|
|
0ee81d0739 | ||
|
|
c0bc6f8ccb | ||
|
|
1f1cd1e236 | ||
|
|
dbbd78fb26 | ||
|
|
f6e45842fd | ||
|
|
d25c043046 | ||
|
|
5695ba7c13 | ||
|
|
eeb0322ce1 | ||
|
|
480977c2b5 | ||
|
|
8b2e46a6fa | ||
|
|
842be660b0 | ||
|
|
4f69c0e502 | ||
|
|
be128181e2 | ||
|
|
0aa1628925 | ||
|
|
fd58f669d0 | ||
|
|
51927f82c0 | ||
|
|
0f8a0eaf4a | ||
|
|
b9ac22d4ed | ||
|
|
ebca5c6409 | ||
|
|
6ef35e4dba | ||
|
|
8bb5200dd8 | ||
|
|
4aeb6b7c9b | ||
|
|
ae64d7ba82 | ||
|
|
9dd2dbff19 | ||
|
|
cebbffe230 | ||
|
|
e3e464abfa | ||
|
|
268438b0ff | ||
|
|
355c8de5e7 | ||
|
|
e6056bc4a4 | ||
|
|
f2eebe95bd | ||
|
|
3084013269 | ||
|
|
68568e5a25 | ||
|
|
0eae0bb8fe | ||
|
|
b61820d354 | ||
|
|
1606272850 | ||
|
|
ea04fe1c9b | ||
|
|
4c50b6a8af | ||
|
|
39aa848e9e | ||
|
|
64c56c1b17 | ||
|
|
8e883a6b10 | ||
|
|
9684aa3ad5 | ||
|
|
5fede84bf6 | ||
|
|
a788e07be1 | ||
|
|
68ffd2248b | ||
|
|
4b12d021e4 | ||
|
|
4daf9db9db | ||
|
|
a660b1eb05 | ||
|
|
6f2f098e12 | ||
|
|
6a75ef86b0 | ||
|
|
3d2397879c | ||
|
|
de474089a7 | ||
|
|
8b31b9aa05 | ||
|
|
ad41672958 | ||
|
|
86f2984b30 | ||
|
|
38e62786e4 | ||
|
|
ac3d7c9fae | ||
|
|
98f94ea694 | ||
|
|
8ad80f7ac3 | ||
|
|
296678046c | ||
|
|
fd193e651d | ||
|
|
becad5ee67 | ||
|
|
161dcbdd4c | ||
|
|
7ad6652cb0 | ||
|
|
c467167e91 | ||
|
|
18093824d8 | ||
|
|
8102f3c33c | ||
|
|
902c441248 | ||
|
|
e641bb3d57 | ||
|
|
19d938ed60 | ||
|
|
5561949f9b | ||
|
|
5e98cca062 | ||
|
|
ac858c9bee | ||
|
|
f58f5e4e02 | ||
|
|
ad844f5e51 | ||
|
|
53c1bc45d6 | ||
|
|
0cc93aa87e | ||
|
|
b41b830086 |
@@ -38,6 +38,7 @@ BASECAMP3_USER_AGENT=
|
||||
DAILY_API_KEY=
|
||||
DAILY_SCALE_PLAN=''
|
||||
DAILY_WEBHOOK_SECRET=''
|
||||
DAILY_MEETING_ENDED_WEBHOOK_SECRET=''
|
||||
|
||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||
# Needed to enable Google Calendar integration and Login with Google
|
||||
|
||||
+5
-1
@@ -240,7 +240,7 @@ CSP_POLICY=
|
||||
# Vercel Edge Config
|
||||
EDGE_CONFIG=
|
||||
|
||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||
NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD=0 # Override the booker to only load X number of days worth of data
|
||||
|
||||
# Control time intervals on a user's Schedule availability
|
||||
@@ -347,6 +347,10 @@ SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
|
||||
# api v2
|
||||
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
|
||||
|
||||
# Tasker features
|
||||
TASKER_ENABLE_WEBHOOKS=0
|
||||
TASKER_ENABLE_EMAILS=0
|
||||
|
||||
# Ratelimiting via unkey
|
||||
UNKEY_ROOT_KEY=
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Cron - changeTimeZone
|
||||
|
||||
on:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||
schedule:
|
||||
# Runs "At every full hour." (see https://crontab.guru)
|
||||
- cron: "0 * * * *"
|
||||
|
||||
jobs:
|
||||
cron-scheduleEmailReminders:
|
||||
env:
|
||||
APP_URL: ${{ secrets.APP_URL }}
|
||||
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: cURL request
|
||||
if: ${{ env.APP_URL && env.CRON_API_KEY }}
|
||||
run: |
|
||||
curl ${{ secrets.APP_URL }}/api/cron/changeTimeZone \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
-sSf
|
||||
@@ -62,6 +62,7 @@ jobs:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
@@ -91,6 +92,7 @@ jobs:
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -49,6 +49,7 @@ jobs:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
@@ -73,6 +74,7 @@ jobs:
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -55,6 +55,7 @@ jobs:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
@@ -80,6 +81,7 @@ jobs:
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -53,6 +53,7 @@ jobs:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
@@ -82,6 +83,7 @@ jobs:
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
@@ -31,6 +32,7 @@ env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
@@ -35,6 +36,7 @@ env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { get } from "@vercel/edge-config";
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
const safeGet = async <T = unknown>(key: string): Promise<T | undefined> => {
|
||||
try {
|
||||
return get<T>(key);
|
||||
} catch (error) {
|
||||
// Don't crash if EDGE_CONFIG env var is missing
|
||||
}
|
||||
};
|
||||
|
||||
export const config = { matcher: "/:path*" };
|
||||
|
||||
export const checkIsInMaintenanceMode: NextMiddleware = async (req, res, next) => {
|
||||
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
|
||||
if (isInMaintenanceMode) {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: "API is currently under maintenance. Please try again at a later time." });
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { label } from "next-api-middleware";
|
||||
|
||||
import { addRequestId } from "./addRequestid";
|
||||
import { captureErrors } from "./captureErrors";
|
||||
import { checkIsInMaintenanceMode } from "./checkIsInMaintenanceMode";
|
||||
import { extendRequest } from "./extendRequest";
|
||||
import {
|
||||
HTTP_POST,
|
||||
@@ -24,6 +25,7 @@ const middleware = {
|
||||
HTTP_POST,
|
||||
HTTP_DELETE,
|
||||
addRequestId,
|
||||
checkIsInMaintenanceMode,
|
||||
verifyApiKey,
|
||||
rateLimitApiKey,
|
||||
extendRequest,
|
||||
@@ -36,6 +38,7 @@ type Middleware = keyof typeof middleware;
|
||||
|
||||
const middlewareOrder = [
|
||||
// The order here, determines the order of execution
|
||||
"checkIsInMaintenanceMode",
|
||||
"extendRequest",
|
||||
"captureErrors",
|
||||
"verifyApiKey",
|
||||
|
||||
@@ -82,7 +82,6 @@ export const schemaUserBaseBodyParams = User.pick({
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
allowDynamicBooking: true,
|
||||
away: true,
|
||||
role: true,
|
||||
// @note: disallowing avatar changes via API for now. We can add it later if needed. User should upload image via UI.
|
||||
// avatar: true,
|
||||
@@ -171,7 +170,6 @@ export const schemaUserReadPublic = User.pick({
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
allowDynamicBooking: true,
|
||||
away: true,
|
||||
createdDate: true,
|
||||
verified: true,
|
||||
invitedTo: true,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppLoggerMiddleware } from "@/app.logger.middleware";
|
||||
import { RewriterMiddleware } from "@/app.rewrites.middleware";
|
||||
import appConfig from "@/config/app";
|
||||
import { AuthModule } from "@/modules/auth/auth.module";
|
||||
import { EndpointsModule } from "@/modules/endpoints.module";
|
||||
@@ -6,6 +7,7 @@ import { JwtModule } from "@/modules/jwt/jwt.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { RouterModule } from "@nestjs/core";
|
||||
|
||||
import { AppController } from "./app.controller";
|
||||
|
||||
@@ -34,11 +36,14 @@ import { AppController } from "./app.controller";
|
||||
EndpointsModule,
|
||||
AuthModule,
|
||||
JwtModule,
|
||||
//register prefix for all routes in EndpointsModule
|
||||
RouterModule.register([{ path: "/v2", module: EndpointsModule }]),
|
||||
],
|
||||
controllers: [AppController],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer): void {
|
||||
consumer.apply(AppLoggerMiddleware).forRoutes("*");
|
||||
consumer.apply(RewriterMiddleware).forRoutes("/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Injectable, NestMiddleware } from "@nestjs/common";
|
||||
import { Request, Response } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class RewriterMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: () => void) {
|
||||
if (req.url.startsWith("/api/v2")) {
|
||||
req.url = req.url.replace("/api/v2", "/v2");
|
||||
}
|
||||
if (req.url.startsWith("/v2/ee")) {
|
||||
req.url = req.url.replace("/v2/ee", "/v2");
|
||||
}
|
||||
if (req.url.includes("reccuring")) {
|
||||
req.url = req.url.replace("reccuring", "recurring");
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import * as Sentry from "@sentry/node";
|
||||
import * as cookieParser from "cookie-parser";
|
||||
import helmet from "helmet";
|
||||
|
||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
||||
|
||||
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
|
||||
@@ -26,7 +28,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
app.enableCors({
|
||||
origin: "*",
|
||||
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
|
||||
allowedHeaders: ["Accept", "Authorization", "Content-Type", "Origin"],
|
||||
allowedHeaders: [X_CAL_CLIENT_ID, X_CAL_SECRET_KEY, "Accept", "Authorization", "Content-Type", "Origin"],
|
||||
maxAge: 86_400,
|
||||
});
|
||||
|
||||
@@ -58,10 +60,6 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
app.useGlobalFilters(new TRPCExceptionFilter());
|
||||
|
||||
app.setGlobalPrefix("api", {
|
||||
exclude: [{ path: "health", method: RequestMethod.GET }],
|
||||
});
|
||||
|
||||
app.use(cookieParser());
|
||||
|
||||
return app;
|
||||
|
||||
@@ -14,7 +14,7 @@ const loadConfig = (): AppConfig => {
|
||||
process.env.API_PORT && getEnv("NODE_ENV", "development") === "development"
|
||||
? `:${Number(getEnv("API_PORT", "5555"))}`
|
||||
: ""
|
||||
}/api/v2`,
|
||||
}/v2`,
|
||||
},
|
||||
db: {
|
||||
readUrl: getEnv("DATABASE_READ_URL"),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BookingsController } from "@/ee/bookings/controllers/bookings.controller";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
@@ -7,7 +8,7 @@ import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, TokensModule],
|
||||
providers: [TokensRepository, OAuthFlowService],
|
||||
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository],
|
||||
controllers: [BookingsController],
|
||||
})
|
||||
export class BookingsModule {}
|
||||
|
||||
@@ -115,7 +115,7 @@ describe("Bookings Endpoints", () => {
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings")
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
@@ -138,7 +138,7 @@ describe("Bookings Endpoints", () => {
|
||||
|
||||
it("should get bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/api/v2/ee/bookings?filters[status]=upcoming")
|
||||
.get("/v2/bookings?filters[status]=upcoming")
|
||||
.then((response) => {
|
||||
console.log("asap responseBody", JSON.stringify(response.body, null, 2));
|
||||
const responseBody: GetBookingsOutput = response.body;
|
||||
@@ -159,7 +159,7 @@ describe("Bookings Endpoints", () => {
|
||||
|
||||
it("should get booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/bookings/${createdBooking.uid}`)
|
||||
.get(`/v2/bookings/${createdBooking.uid}`)
|
||||
.then((response) => {
|
||||
const responseBody: GetBookingOutput = response.body;
|
||||
const bookingInfo = responseBody.data;
|
||||
@@ -199,7 +199,7 @@ describe("Bookings Endpoints", () => {
|
||||
// };
|
||||
|
||||
// return request(app.getHttpServer())
|
||||
// .post("/api/v2/ee/bookings/reccuring")
|
||||
// .post("/v2/bookings/recurring")
|
||||
// .send(body)
|
||||
// .expect(201)
|
||||
// .then((response) => {
|
||||
@@ -231,7 +231,7 @@ describe("Bookings Endpoints", () => {
|
||||
// };
|
||||
|
||||
// return request(app.getHttpServer())
|
||||
// .post("/api/v2/ee/bookings/instant")
|
||||
// .post("/v2/bookings/instant")
|
||||
// .send(body)
|
||||
// .expect(201)
|
||||
// .then((response) => {
|
||||
@@ -250,7 +250,7 @@ describe("Bookings Endpoints", () => {
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/api/v2/ee/bookings/${bookingId}/cancel`)
|
||||
.post(`/v2/bookings/${bookingId}/cancel`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { CreateReccuringBookingInput } from "@/ee/bookings/inputs/create-reccuring-booking.input";
|
||||
import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input";
|
||||
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
||||
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||
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 { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import {
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
Req,
|
||||
InternalServerErrorException,
|
||||
Body,
|
||||
Headers,
|
||||
HttpException,
|
||||
Param,
|
||||
Get,
|
||||
@@ -27,6 +29,7 @@ import { User } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { NextApiRequest } from "next/types";
|
||||
|
||||
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
|
||||
import { BOOKING_READ, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
getAllUserBookings,
|
||||
@@ -45,8 +48,30 @@ import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-t
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
type BookingRequest = Request & {
|
||||
userId?: number;
|
||||
};
|
||||
|
||||
type OAuthRequestParams = {
|
||||
platformClientId: string;
|
||||
platformRescheduleUrl: string;
|
||||
platformCancelUrl: string;
|
||||
platformBookingUrl: string;
|
||||
platformBookingLocation?: string;
|
||||
arePlatformEmailsEnabled: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_PLATFORM_PARAMS = {
|
||||
platformClientId: "",
|
||||
platformCancelUrl: "",
|
||||
platformRescheduleUrl: "",
|
||||
platformBookingUrl: "",
|
||||
arePlatformEmailsEnabled: false,
|
||||
platformBookingLocation: undefined,
|
||||
};
|
||||
|
||||
@Controller({
|
||||
path: "ee/bookings",
|
||||
path: "/bookings",
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(PermissionsGuard)
|
||||
@@ -56,7 +81,8 @@ export class BookingsController {
|
||||
|
||||
constructor(
|
||||
private readonly oAuthFlowService: OAuthFlowService,
|
||||
private readonly prismaReadService: PrismaReadService
|
||||
private readonly prismaReadService: PrismaReadService,
|
||||
private readonly oAuthClientRepository: OAuthClientRepository
|
||||
) {}
|
||||
|
||||
@Get("/")
|
||||
@@ -117,13 +143,16 @@ export class BookingsController {
|
||||
|
||||
@Post("/")
|
||||
async createBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Body() _: CreateBookingInput
|
||||
@Req() req: BookingRequest,
|
||||
@Body() body: CreateBookingInput,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<unknown>> {
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
req.body = { ...req.body, noEmail: true };
|
||||
const oAuthClientId = clientId?.toString();
|
||||
const locationUrl = body.locationUrl;
|
||||
try {
|
||||
const booking = await handleNewBooking(req as unknown as NextApiRequest & { userId?: number });
|
||||
const booking = await handleNewBooking(
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
@@ -136,15 +165,16 @@ export class BookingsController {
|
||||
|
||||
@Post("/:bookingId/cancel")
|
||||
async cancelBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Req() req: BookingRequest,
|
||||
@Param("bookingId") bookingId: string,
|
||||
@Body() body: CancelBookingInput
|
||||
@Body() _: CancelBookingInput,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
if (bookingId) {
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
req.body = { ...body, id: parseInt(bookingId) };
|
||||
try {
|
||||
await handleCancelBooking(req as unknown as NextApiRequest & { userId?: number });
|
||||
req.body.id = parseInt(bookingId);
|
||||
await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
@@ -157,16 +187,16 @@ export class BookingsController {
|
||||
throw new InternalServerErrorException("Could not cancel booking.");
|
||||
}
|
||||
|
||||
@Post("/reccuring")
|
||||
async createReccuringBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Body() _: CreateReccuringBookingInput[]
|
||||
@Post("/recurring")
|
||||
async createRecurringBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateRecurringBookingInput[],
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<BookingResponse[]>> {
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
req.body = { ...req.body, noEmail: true };
|
||||
const oAuthClientId = clientId?.toString();
|
||||
try {
|
||||
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
|
||||
req as unknown as NextApiRequest & { userId?: number }
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -180,14 +210,15 @@ export class BookingsController {
|
||||
|
||||
@Post("/instant")
|
||||
async createInstantBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Body() _: CreateBookingInput
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateBookingInput,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
req.body = { ...req.body, noEmail: true };
|
||||
try {
|
||||
const instantMeeting = await handleInstantMeeting(
|
||||
req as unknown as NextApiRequest & { userId?: number }
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -209,6 +240,39 @@ export class BookingsController {
|
||||
this.logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getOAuthClientsParams(req: BookingRequest, clientId: string): Promise<OAuthRequestParams> {
|
||||
const res = DEFAULT_PLATFORM_PARAMS;
|
||||
try {
|
||||
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
|
||||
// fetch oAuthClient from db and use data stored in db to set these values
|
||||
if (client) {
|
||||
res.platformClientId = clientId;
|
||||
res.platformCancelUrl = client.bookingCancelRedirectUri ?? "";
|
||||
res.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
|
||||
res.platformBookingUrl = client.bookingRedirectUri ?? "";
|
||||
res.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false;
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
async createNextApiBookingRequest(
|
||||
req: BookingRequest,
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(req, oAuthClientId)
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
Object.assign(req, { userId, ...oAuthParams, platformBookingLocation });
|
||||
req.body = { ...req.body, noEmail: !oAuthParams.arePlatformEmailsEnabled };
|
||||
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
}
|
||||
}
|
||||
|
||||
function handleBookingErrors(err: Error | HttpError | unknown, type?: "recurring" | `instant`): void {
|
||||
|
||||
@@ -98,4 +98,8 @@ export class CreateBookingInput {
|
||||
|
||||
@Type(() => Response)
|
||||
responses!: Response;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
locationUrl?: string;
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
|
||||
export class CreateReccuringBookingInput extends CreateBookingInput {
|
||||
export class CreateRecurringBookingInput extends CreateBookingInput {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
noEmail?: boolean;
|
||||
@@ -11,7 +11,7 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { CalendarBusyTimesInput } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "ee/calendars",
|
||||
path: "/calendars",
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(AccessTokenGuard)
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CredentialsWithUserEmail,
|
||||
} from "@/modules/credentials/credentials.repository";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Injectable,
|
||||
@@ -23,7 +24,8 @@ export class CalendarsService {
|
||||
constructor(
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly credentialsRepository: CredentialsRepository,
|
||||
private readonly dbRead: PrismaReadService
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly dbWrite: PrismaWriteService
|
||||
) {}
|
||||
|
||||
async getCalendars(userId: number) {
|
||||
@@ -35,7 +37,7 @@ export class CalendarsService {
|
||||
return getConnectedDestinationCalendars(
|
||||
userWithCalendars,
|
||||
false,
|
||||
this.dbRead.prisma as unknown as PrismaClient
|
||||
this.dbWrite.prisma as unknown as PrismaClient
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import { AppModule } from "@/app.module";
|
||||
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
|
||||
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
||||
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
@@ -64,7 +66,7 @@ describe("Event types Endpoints", () => {
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
|
||||
const userEmail = "event-types-test-e2e@api.com";
|
||||
const name = "bob the builder";
|
||||
const name = "bob-the-builder";
|
||||
const username = name;
|
||||
let eventType: EventType;
|
||||
let user: User;
|
||||
@@ -183,6 +185,41 @@ describe("Event types Endpoints", () => {
|
||||
expect(responseBody.data.eventType.userId).toEqual(user.id);
|
||||
});
|
||||
|
||||
it(`/GET/:username/public`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${username}/public`)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetEventTypesPublicOutput = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(1);
|
||||
expect(responseBody.data?.[0]?.id).toEqual(eventType.id);
|
||||
expect(responseBody.data?.[0]?.title).toEqual(eventType.title);
|
||||
expect(responseBody.data?.[0]?.slug).toEqual(eventType.slug);
|
||||
expect(responseBody.data?.[0]?.length).toEqual(eventType.length);
|
||||
});
|
||||
|
||||
it(`/GET/:username/:eventSlug/public`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${username}/${eventType.slug}/public`)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetEventTypePublicOutput = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.id).toEqual(eventType.id);
|
||||
expect(responseBody.data?.title).toEqual(eventType.title);
|
||||
expect(responseBody.data?.slug).toEqual(eventType.slug);
|
||||
expect(responseBody.data?.length).toEqual(eventType.length);
|
||||
});
|
||||
|
||||
it(`/GET/`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types`)
|
||||
@@ -202,7 +239,7 @@ describe("Event types Endpoints", () => {
|
||||
expect(responseBody.data.profiles?.[0]?.name).toEqual(name);
|
||||
});
|
||||
|
||||
it(`/GET/:username/public`, async () => {
|
||||
it(`/GET/public/:username/`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${username}/public`)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.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 { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
|
||||
import { DeleteEventTypeOutput } from "@/ee/event-types/outputs/delete-event-type.output";
|
||||
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
|
||||
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
||||
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
|
||||
import { GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
|
||||
@@ -11,6 +13,7 @@ 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 { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Controller,
|
||||
@@ -24,11 +27,15 @@ import {
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Delete,
|
||||
Query,
|
||||
InternalServerErrorException,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { getPublicEvent } from "@calcom/platform-libraries";
|
||||
import { getEventTypesByViewer } from "@calcom/platform-libraries";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Controller({
|
||||
path: "event-types",
|
||||
@@ -37,7 +44,10 @@ import { getEventTypesByViewer } from "@calcom/platform-libraries";
|
||||
@UseGuards(PermissionsGuard)
|
||||
@DocsTags("Event types")
|
||||
export class EventTypesController {
|
||||
constructor(private readonly eventTypesService: EventTypesService) {}
|
||||
constructor(
|
||||
private readonly eventTypesService: EventTypesService,
|
||||
private readonly prismaReadService: PrismaReadService
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@Permissions([EVENT_TYPE_WRITE])
|
||||
@@ -90,8 +100,36 @@ export class EventTypesController {
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:username/:eventSlug/public")
|
||||
async getPublicEventType(
|
||||
@Param("username") username: string,
|
||||
@Param("eventSlug") eventSlug: string,
|
||||
@Query() queryParams: GetPublicEventTypeQueryParams
|
||||
): Promise<GetEventTypePublicOutput> {
|
||||
try {
|
||||
const event = await getPublicEvent(
|
||||
username.toLowerCase(),
|
||||
eventSlug,
|
||||
queryParams.isTeamEvent,
|
||||
queryParams.org || null,
|
||||
this.prismaReadService.prisma as unknown as PrismaClient,
|
||||
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
|
||||
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
|
||||
true
|
||||
);
|
||||
return {
|
||||
data: event,
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
throw new NotFoundException(err.message);
|
||||
}
|
||||
}
|
||||
throw new InternalServerErrorException("Could not find public event.");
|
||||
}
|
||||
|
||||
@Get("/:username/public")
|
||||
@Permissions([EVENT_TYPE_READ])
|
||||
async getPublicEventTypes(@Param("username") username: string): Promise<GetEventTypesPublicOutput> {
|
||||
const eventTypes = await this.eventTypesService.getEventTypesPublicByUsername(username);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
@@ -48,9 +49,7 @@ export class EventTypesRepository {
|
||||
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
|
||||
eventTypeId,
|
||||
userId: user.id,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
prisma: this.dbRead.prisma,
|
||||
prisma: this.dbRead.prisma as unknown as PrismaClient,
|
||||
isUserOrganizationAdmin,
|
||||
isTrpcCall: true,
|
||||
});
|
||||
|
||||
+1
-10
@@ -2,16 +2,7 @@ import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsBoolean, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class GetPublicEventInput {
|
||||
@IsString()
|
||||
@Transform(({ value }: { value: string }) => value.toLowerCase())
|
||||
@ApiProperty({ required: true })
|
||||
username!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ required: true })
|
||||
eventSlug!: string;
|
||||
|
||||
export class GetPublicEventTypeQueryParams {
|
||||
@Transform(({ value }: { value: string }) => value === "true")
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@@ -0,0 +1,355 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
ValidateNested,
|
||||
IsArray,
|
||||
IsObject,
|
||||
IsNumber,
|
||||
IsEnum,
|
||||
} from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
type!: string;
|
||||
}
|
||||
|
||||
class Source {
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
label!: string;
|
||||
}
|
||||
|
||||
class OptionInput {
|
||||
@IsString()
|
||||
type!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
class BookingField {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
defaultLabel?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
label?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
placeholder?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
getOptionsAt?: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
optionsInputs?: { [key: string]: OptionInput };
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
hideWhenJustOneOption?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
editable?: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Source)
|
||||
@IsOptional()
|
||||
sources?: Source[];
|
||||
}
|
||||
|
||||
class Organization {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
slug?: string | null;
|
||||
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
metadata!: Record<string, any>;
|
||||
}
|
||||
|
||||
class Profile {
|
||||
@IsString()
|
||||
username!: string | null;
|
||||
|
||||
@IsInt()
|
||||
id!: number | null;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
userId?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
uid?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsInt()
|
||||
organizationId!: number | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Organization)
|
||||
organization?: Organization | null;
|
||||
|
||||
@IsString()
|
||||
upId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
image?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
brandColor?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
darkBrandColor?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
bookerLayouts?: any;
|
||||
}
|
||||
|
||||
class Owner {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
avatarUrl?: string | null;
|
||||
|
||||
@IsString()
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
name!: string | null;
|
||||
|
||||
@IsString()
|
||||
weekStart!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
brandColor?: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
darkBrandColor?: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
theme?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
metadata!: any;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
defaultScheduleId?: number | null;
|
||||
|
||||
@IsString()
|
||||
nonProfileUsername!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Profile)
|
||||
profile!: Profile;
|
||||
}
|
||||
|
||||
class User {
|
||||
@IsString()
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
name!: string | null;
|
||||
|
||||
@IsString()
|
||||
weekStart!: string;
|
||||
|
||||
@IsInt()
|
||||
organizationId?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
avatarUrl?: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
profile!: Profile;
|
||||
|
||||
@IsString()
|
||||
bookerUrl!: string;
|
||||
}
|
||||
|
||||
class Schedule {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
timeZone!: string | null;
|
||||
}
|
||||
|
||||
class PublicEventTypeOutput {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
description!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
eventName?: string | null;
|
||||
|
||||
@IsString()
|
||||
slug!: string;
|
||||
|
||||
@IsBoolean()
|
||||
isInstantEvent!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
aiPhoneCallConfig?: any;
|
||||
|
||||
@IsOptional()
|
||||
schedulingType?: any;
|
||||
|
||||
@IsInt()
|
||||
length!: number;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Location)
|
||||
locations!: Location[];
|
||||
|
||||
@IsArray()
|
||||
customInputs!: any[];
|
||||
|
||||
@IsBoolean()
|
||||
disableGuests!: boolean;
|
||||
|
||||
@IsObject()
|
||||
metadata!: object | null;
|
||||
|
||||
@IsBoolean()
|
||||
lockTimeZoneToggleOnBookingPage!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
requiresConfirmation!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
requiresBookerEmailVerification!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
recurringEvent?: any;
|
||||
|
||||
@IsNumber()
|
||||
price!: number;
|
||||
|
||||
@IsString()
|
||||
currency!: string;
|
||||
|
||||
@IsOptional()
|
||||
seatsPerTimeSlot?: number | null;
|
||||
|
||||
@IsBoolean()
|
||||
seatsShowAvailabilityCount!: boolean | null;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BookingField)
|
||||
bookingFields!: BookingField[];
|
||||
|
||||
@IsOptional()
|
||||
team?: any;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
successRedirectUrl?: string | null;
|
||||
|
||||
@IsArray()
|
||||
workflows!: any[];
|
||||
|
||||
@IsArray()
|
||||
hosts!: any[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Owner)
|
||||
owner!: Owner | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Schedule)
|
||||
schedule!: Schedule | null;
|
||||
|
||||
@IsBoolean()
|
||||
hidden!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
assignAllTeamMembers!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
bookerLayouts?: any;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => User)
|
||||
users!: User[];
|
||||
|
||||
@IsObject()
|
||||
entity!: object;
|
||||
|
||||
@IsBoolean()
|
||||
isDynamic!: boolean;
|
||||
}
|
||||
|
||||
export class GetEventTypePublicOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PublicEventTypeOutput)
|
||||
@IsArray()
|
||||
data!: PublicEventTypeOutput | null;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -18,6 +19,20 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
||||
|
||||
const CLIENT_REDIRECT_URI = "http://localhost:5555";
|
||||
|
||||
class CalendarsServiceMock {
|
||||
async getCalendars() {
|
||||
return {
|
||||
connectedCalendars: [
|
||||
{
|
||||
integration: {
|
||||
type: "google_calendar",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe("Platform Gcal Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
@@ -42,6 +57,8 @@ describe("Platform Gcal Endpoints", () => {
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.overrideProvider(CalendarsService)
|
||||
.useClass(CalendarsServiceMock)
|
||||
.compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
@@ -85,14 +102,14 @@ describe("Platform Gcal Endpoints", () => {
|
||||
|
||||
it(`/GET/ee/gcal/oauth/auth-url: it should respond 401 with invalid access token`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
||||
.get(`/v2/gcal/oauth/auth-url`)
|
||||
.set("Authorization", `Bearer invalid_access_token`)
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it(`/GET/ee/gcal/oauth/auth-url: it should auth-url to google oauth with valid access token `, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
||||
.get(`/v2/gcal/oauth/auth-url`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(200);
|
||||
@@ -103,7 +120,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without oauth code`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/ee/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3D${CLIENT_REDIRECT_URI}&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
`/v2/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3D${CLIENT_REDIRECT_URI}&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
@@ -111,7 +128,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without access token`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/ee/gcal/oauth/save?state=origin%3D${CLIENT_REDIRECT_URI}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
`/v2/gcal/oauth/save?state=origin%3D${CLIENT_REDIRECT_URI}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
@@ -119,25 +136,25 @@ describe("Platform Gcal Endpoints", () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without origin`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/ee/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
`/v2/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/ee/gcal/check with access token but without origin`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.get(`/v2/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/ee/gcal/check without access token`, async () => {
|
||||
await request(app.getHttpServer()).get(`/api/v2/ee/gcal/check`).expect(401);
|
||||
await request(app.getHttpServer()).get(`/v2/gcal/check`).expect(401);
|
||||
});
|
||||
|
||||
it(`/GET/ee/gcal/check with access token and origin but no credentials`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.get(`/v2/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(400);
|
||||
@@ -151,7 +168,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
"google-calendar"
|
||||
);
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.get(`/v2/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(200);
|
||||
|
||||
@@ -44,7 +44,7 @@ const CALENDAR_SCOPES = [
|
||||
|
||||
// Controller for the GCalConnect Atom
|
||||
@Controller({
|
||||
path: "ee/gcal",
|
||||
path: "/gcal",
|
||||
version: "2",
|
||||
})
|
||||
@DocsTags("Google Calendar")
|
||||
@@ -60,7 +60,7 @@ export class GcalController {
|
||||
private readonly calendarsService: CalendarsService
|
||||
) {}
|
||||
|
||||
private redirectUri = `${this.config.get("api.url")}/ee/gcal/oauth/save`;
|
||||
private redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
|
||||
|
||||
@Get("/oauth/auth-url")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
@@ -71,7 +71,7 @@ describe("Me Endpoints", () => {
|
||||
|
||||
it("should get user associated with access token", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/api/v2/ee/me")
|
||||
.get("/v2/me")
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<UserResponse> = response.body;
|
||||
@@ -90,7 +90,7 @@ describe("Me Endpoints", () => {
|
||||
const body: UpdateManagedUserInput = { timeZone: "Europe/Rome" };
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch("/api/v2/ee/me")
|
||||
.patch("/v2/me")
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
@@ -114,22 +114,19 @@ describe("Me Endpoints", () => {
|
||||
it("should not update user associated with access token given invalid timezone", async () => {
|
||||
const bodyWithIncorrectTimeZone: UpdateManagedUserInput = { timeZone: "Narnia/Woods" };
|
||||
|
||||
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectTimeZone).expect(400);
|
||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeZone).expect(400);
|
||||
});
|
||||
|
||||
it("should not update user associated with access token given invalid time format", async () => {
|
||||
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 };
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch("/api/v2/ee/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 () => {
|
||||
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" };
|
||||
|
||||
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { PROFILE_READ, PROFILE_WRITE, SUCCESS_STATUS } from "@calcom/platform-co
|
||||
import { userSchemaResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "ee/me",
|
||||
path: "/me",
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
@Controller({
|
||||
path: "ee/provider",
|
||||
path: "/provider",
|
||||
version: "2",
|
||||
})
|
||||
@DocsTags("Cal provider")
|
||||
|
||||
@@ -19,12 +19,17 @@ export class CreateAvailabilityInput {
|
||||
}
|
||||
|
||||
function transformStringToDate(value: string, key: string): Date {
|
||||
const parts = value.split(":");
|
||||
// note(Lauris): incoming value is ISO8061 e.g. 2025-0412T13:17:56.324Z
|
||||
const dateTimeParts = value.split("T");
|
||||
|
||||
const timePart = dateTimeParts[1].split(".")[0]; // Removes milliseconds
|
||||
const parts = timePart.split(":");
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw new BadRequestException(`Invalid ${key} format. Expected format: HH:MM:SS. Received ${value}`);
|
||||
throw new BadRequestException(
|
||||
`Invalid time format. Expected format(ISO8061): 2025-0412T13:17:56.324Z. Received: ${value}`
|
||||
);
|
||||
}
|
||||
|
||||
const [hours, minutes, seconds] = parts.map(Number);
|
||||
|
||||
if (hours < 0 || hours > 23) {
|
||||
|
||||
@@ -27,7 +27,7 @@ export class CredentialsRepository {
|
||||
invalid: false,
|
||||
},
|
||||
where: {
|
||||
id: credential?.id,
|
||||
id: credential?.id ?? 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
|
||||
import { EventsModule } from "@/modules/events/events.module";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [EventsModule, OAuthClientModule, PlatformEndpointsModule, TimezoneModule],
|
||||
imports: [OAuthClientModule, PlatformEndpointsModule, TimezoneModule],
|
||||
})
|
||||
export class EndpointsModule implements NestModule {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { Controller, Get, NotFoundException, InternalServerErrorException, Query } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { getPublicEvent } from "@calcom/platform-libraries";
|
||||
import type { PublicEventType } from "@calcom/platform-libraries";
|
||||
import { ApiResponse, GetPublicEventInput } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Controller({
|
||||
path: "events",
|
||||
version: "2",
|
||||
})
|
||||
@DocsTags("Event types")
|
||||
export class EventsController {
|
||||
constructor(private readonly prismaReadService: PrismaReadService) {}
|
||||
|
||||
@Get("/public")
|
||||
async getPublicEvent(@Query() queryParams: GetPublicEventInput): Promise<ApiResponse<PublicEventType>> {
|
||||
try {
|
||||
const event = await getPublicEvent(
|
||||
queryParams.username,
|
||||
queryParams.eventSlug,
|
||||
queryParams.isTeamEvent,
|
||||
queryParams.org || null,
|
||||
this.prismaReadService.prisma as unknown as PrismaClient,
|
||||
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
|
||||
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
|
||||
true
|
||||
);
|
||||
return {
|
||||
data: event,
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
throw new NotFoundException(err.message);
|
||||
}
|
||||
}
|
||||
throw new InternalServerErrorException("Could not find public event.");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { EventsController } from "@/modules/events/controllers/events.controller";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [EventsController],
|
||||
})
|
||||
export class EventsModule {}
|
||||
+3
-3
@@ -199,7 +199,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
it(`/GET/:id`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||
.expect(200);
|
||||
|
||||
@@ -216,7 +216,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.patch(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||
.send(body)
|
||||
.expect(200);
|
||||
@@ -231,7 +231,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
it(`/DELETE/:id`, () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
-5
@@ -76,11 +76,6 @@ export class OAuthClientUsersController {
|
||||
this.logger.log(
|
||||
`Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}`
|
||||
);
|
||||
const existingUser = await this.userRepository.findByEmail(body.email);
|
||||
|
||||
if (existingUser) {
|
||||
throw new BadRequestException("A user with the provided email already exists.");
|
||||
}
|
||||
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
|
||||
|
||||
const isPlatformManaged = true;
|
||||
|
||||
@@ -23,7 +23,7 @@ export class OAuthClientUsersService {
|
||||
isPlatformManaged: boolean,
|
||||
organizationId?: number
|
||||
) {
|
||||
const existsWithEmail = await this.userExistsWithEmail(oAuthClientId, body.email);
|
||||
const existsWithEmail = await this.managedUserExistsWithEmail(oAuthClientId, body.email);
|
||||
if (existsWithEmail) {
|
||||
throw new BadRequestException("User with the provided e-mail already exists.");
|
||||
}
|
||||
@@ -73,7 +73,7 @@ export class OAuthClientUsersService {
|
||||
};
|
||||
}
|
||||
|
||||
async userExistsWithEmail(oAuthClientId: string, email: string) {
|
||||
async managedUserExistsWithEmail(oAuthClientId: string, email: string) {
|
||||
const oAuthEmail = this.getOAuthUserEmail(oAuthClientId, email);
|
||||
const user = await this.userRepository.findByEmail(oAuthEmail);
|
||||
return !!user;
|
||||
|
||||
@@ -22,61 +22,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/events/public": {
|
||||
"get": {
|
||||
"operationId": "EventsController_getPublicEvent",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "eventSlug",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isTeamEvent",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "org",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth-clients/{clientId}/users": {
|
||||
"/v2/oauth-clients/{clientId}/users": {
|
||||
"get": {
|
||||
"operationId": "OAuthClientUsersController_getManagedUsers",
|
||||
"parameters": [
|
||||
@@ -144,7 +90,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth-clients/{clientId}/users/{userId}": {
|
||||
"/v2/oauth-clients/{clientId}/users/{userId}": {
|
||||
"get": {
|
||||
"operationId": "OAuthClientUsersController_getUserById",
|
||||
"parameters": [
|
||||
@@ -264,7 +210,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth-clients/{clientId}/users/{userId}/force-refresh": {
|
||||
"/v2/oauth-clients/{clientId}/users/{userId}/force-refresh": {
|
||||
"post": {
|
||||
"operationId": "OAuthClientUsersController_forceRefresh",
|
||||
"parameters": [
|
||||
@@ -302,7 +248,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth-clients": {
|
||||
"/v2/oauth-clients": {
|
||||
"post": {
|
||||
"operationId": "OAuthClientsController_createOAuthClient",
|
||||
"summary": "",
|
||||
@@ -356,7 +302,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth-clients/{clientId}": {
|
||||
"/v2/oauth-clients/{clientId}": {
|
||||
"get": {
|
||||
"operationId": "OAuthClientsController_getOAuthClientById",
|
||||
"summary": "",
|
||||
@@ -458,7 +404,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth/{clientId}/authorize": {
|
||||
"/v2/oauth/{clientId}/authorize": {
|
||||
"post": {
|
||||
"operationId": "OAuthFlowController_authorize",
|
||||
"summary": "Authorize an OAuth client",
|
||||
@@ -496,7 +442,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth/{clientId}/exchange": {
|
||||
"/v2/oauth/{clientId}/exchange": {
|
||||
"post": {
|
||||
"operationId": "OAuthFlowController_exchange",
|
||||
"summary": "Exchange authorization code for access tokens",
|
||||
@@ -549,7 +495,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth/{clientId}/refresh": {
|
||||
"/v2/oauth/{clientId}/refresh": {
|
||||
"post": {
|
||||
"operationId": "OAuthFlowController_refreshAccessToken",
|
||||
"parameters": [
|
||||
@@ -597,7 +543,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/event-types": {
|
||||
"/v2/event-types": {
|
||||
"post": {
|
||||
"operationId": "EventTypesController_createEventType",
|
||||
"parameters": [],
|
||||
@@ -647,7 +593,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/event-types/{eventTypeId}": {
|
||||
"/v2/event-types/{eventTypeId}": {
|
||||
"get": {
|
||||
"operationId": "EventTypesController_getEventType",
|
||||
"parameters": [
|
||||
@@ -743,7 +689,61 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/event-types/{username}/public": {
|
||||
"/v2/event-types/{username}/{eventSlug}/public": {
|
||||
"get": {
|
||||
"operationId": "EventTypesController_getPublicEventType",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "eventSlug",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isTeamEvent",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "org",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetEventTypePublicOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/event-types/{username}/public": {
|
||||
"get": {
|
||||
"operationId": "EventTypesController_getPublicEventTypes",
|
||||
"parameters": [
|
||||
@@ -773,7 +773,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/gcal/oauth/auth-url": {
|
||||
"/v2/gcal/oauth/auth-url": {
|
||||
"get": {
|
||||
"operationId": "GcalController_redirect",
|
||||
"parameters": [
|
||||
@@ -803,7 +803,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/gcal/oauth/save": {
|
||||
"/v2/gcal/oauth/save": {
|
||||
"get": {
|
||||
"operationId": "GcalController_save",
|
||||
"parameters": [
|
||||
@@ -841,7 +841,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/gcal/check": {
|
||||
"/v2/gcal/check": {
|
||||
"get": {
|
||||
"operationId": "GcalController_check",
|
||||
"parameters": [],
|
||||
@@ -862,7 +862,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/provider/{clientId}": {
|
||||
"/v2/provider/{clientId}": {
|
||||
"get": {
|
||||
"operationId": "CalProviderController_verifyClientId",
|
||||
"parameters": [
|
||||
@@ -892,7 +892,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/provider/{clientId}/access-token": {
|
||||
"/v2/provider/{clientId}/access-token": {
|
||||
"get": {
|
||||
"operationId": "CalProviderController_verifyAccessToken",
|
||||
"parameters": [
|
||||
@@ -922,7 +922,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/schedules": {
|
||||
"/v2/schedules": {
|
||||
"post": {
|
||||
"operationId": "SchedulesController_createSchedule",
|
||||
"parameters": [],
|
||||
@@ -972,7 +972,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/schedules/default": {
|
||||
"/v2/schedules/default": {
|
||||
"get": {
|
||||
"operationId": "SchedulesController_getDefaultSchedule",
|
||||
"parameters": [],
|
||||
@@ -993,7 +993,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/schedules/{scheduleId}": {
|
||||
"/v2/schedules/{scheduleId}": {
|
||||
"get": {
|
||||
"operationId": "SchedulesController_getSchedule",
|
||||
"parameters": [
|
||||
@@ -1089,7 +1089,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/me": {
|
||||
"/v2/me": {
|
||||
"get": {
|
||||
"operationId": "MeController_getMe",
|
||||
"parameters": [],
|
||||
@@ -1139,7 +1139,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/calendars/busy-times": {
|
||||
"/v2/calendars/busy-times": {
|
||||
"get": {
|
||||
"operationId": "CalendarsController_getBusyTimes",
|
||||
"parameters": [],
|
||||
@@ -1160,7 +1160,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/calendars": {
|
||||
"/v2/calendars": {
|
||||
"get": {
|
||||
"operationId": "CalendarsController_getCalendars",
|
||||
"parameters": [],
|
||||
@@ -1181,7 +1181,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings": {
|
||||
"/v2/bookings": {
|
||||
"get": {
|
||||
"operationId": "BookingsController_getBookings",
|
||||
"parameters": [
|
||||
@@ -1235,7 +1235,16 @@
|
||||
},
|
||||
"post": {
|
||||
"operationId": "BookingsController_createBooking",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "x-cal-client-id",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
@@ -1263,7 +1272,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/{bookingUid}": {
|
||||
"/v2/bookings/{bookingUid}": {
|
||||
"get": {
|
||||
"operationId": "BookingsController_getBooking",
|
||||
"parameters": [
|
||||
@@ -1293,7 +1302,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/{bookingUid}/reschedule": {
|
||||
"/v2/bookings/{bookingUid}/reschedule": {
|
||||
"get": {
|
||||
"operationId": "BookingsController_getBookingForReschedule",
|
||||
"parameters": [
|
||||
@@ -1323,7 +1332,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/{bookingId}/cancel": {
|
||||
"/v2/bookings/{bookingId}/cancel": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_cancelBooking",
|
||||
"parameters": [
|
||||
@@ -1334,6 +1343,14 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "x-cal-client-id",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
@@ -1363,10 +1380,19 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/reccuring": {
|
||||
"/v2/bookings/recurring": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_createReccuringBooking",
|
||||
"parameters": [],
|
||||
"operationId": "BookingsController_createRecurringBooking",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "x-cal-client-id",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
@@ -1397,10 +1423,19 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/instant": {
|
||||
"/v2/bookings/instant": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_createInstantBooking",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "x-cal-client-id",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
@@ -1428,7 +1463,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/slots/reserve": {
|
||||
"/v2/slots/reserve": {
|
||||
"post": {
|
||||
"operationId": "SlotsController_reserveSlot",
|
||||
"parameters": [],
|
||||
@@ -1459,7 +1494,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/slots/selected-slot": {
|
||||
"/v2/slots/selected-slot": {
|
||||
"delete": {
|
||||
"operationId": "SlotsController_deleteSelectedSlot",
|
||||
"parameters": [],
|
||||
@@ -1480,7 +1515,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/slots/available": {
|
||||
"/v2/slots/available": {
|
||||
"get": {
|
||||
"operationId": "SlotsController_getAvailableSlots",
|
||||
"parameters": [],
|
||||
@@ -1501,7 +1536,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/timezones": {
|
||||
"/v2/timezones": {
|
||||
"get": {
|
||||
"operationId": "TimezonesController_getTimeZones",
|
||||
"parameters": [],
|
||||
@@ -1912,6 +1947,18 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"bookingRedirectUri": {
|
||||
"type": "string"
|
||||
},
|
||||
"bookingCancelRedirectUri": {
|
||||
"type": "string"
|
||||
},
|
||||
"bookingRescheduleRedirectUri": {
|
||||
"type": "string"
|
||||
},
|
||||
"areEmailsEnabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2137,6 +2184,462 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"Location": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"Source": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"type",
|
||||
"label"
|
||||
]
|
||||
},
|
||||
"BookingField": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"defaultLabel": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"getOptionsAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"hideWhenJustOneOption": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"editable": {
|
||||
"type": "string"
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Source"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"Organization": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"metadata"
|
||||
]
|
||||
},
|
||||
"Profile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"userId": {
|
||||
"type": "number"
|
||||
},
|
||||
"uid": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organizationId": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"organization": {
|
||||
"nullable": true,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Organization"
|
||||
}
|
||||
]
|
||||
},
|
||||
"upId": {
|
||||
"type": "string"
|
||||
},
|
||||
"image": {
|
||||
"type": "string"
|
||||
},
|
||||
"brandColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"darkBrandColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"type": "string"
|
||||
},
|
||||
"bookerLayouts": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"id",
|
||||
"organizationId",
|
||||
"upId"
|
||||
]
|
||||
},
|
||||
"Owner": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"avatarUrl": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"weekStart": {
|
||||
"type": "string"
|
||||
},
|
||||
"brandColor": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"darkBrandColor": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"theme": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"defaultScheduleId": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"nonProfileUsername": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"profile": {
|
||||
"$ref": "#/components/schemas/Profile"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"username",
|
||||
"name",
|
||||
"weekStart",
|
||||
"metadata",
|
||||
"nonProfileUsername",
|
||||
"profile"
|
||||
]
|
||||
},
|
||||
"Schedule": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"timeZone"
|
||||
]
|
||||
},
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"weekStart": {
|
||||
"type": "string"
|
||||
},
|
||||
"organizationId": {
|
||||
"type": "number"
|
||||
},
|
||||
"avatarUrl": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"profile": {
|
||||
"$ref": "#/components/schemas/Profile"
|
||||
},
|
||||
"bookerUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"name",
|
||||
"weekStart",
|
||||
"profile",
|
||||
"bookerUrl"
|
||||
]
|
||||
},
|
||||
"PublicEventTypeOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"eventName": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"slug": {
|
||||
"type": "string"
|
||||
},
|
||||
"isInstantEvent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"aiPhoneCallConfig": {
|
||||
"type": "object"
|
||||
},
|
||||
"schedulingType": {
|
||||
"type": "object"
|
||||
},
|
||||
"length": {
|
||||
"type": "number"
|
||||
},
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Location"
|
||||
}
|
||||
},
|
||||
"customInputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"disableGuests": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"nullable": true
|
||||
},
|
||||
"lockTimeZoneToggleOnBookingPage": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"requiresConfirmation": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"requiresBookerEmailVerification": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"recurringEvent": {
|
||||
"type": "object"
|
||||
},
|
||||
"price": {
|
||||
"type": "number"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"seatsPerTimeSlot": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"seatsShowAvailabilityCount": {
|
||||
"type": "boolean",
|
||||
"nullable": true
|
||||
},
|
||||
"bookingFields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/BookingField"
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
"type": "object"
|
||||
},
|
||||
"successRedirectUrl": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"workflows": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"owner": {
|
||||
"nullable": true,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Owner"
|
||||
}
|
||||
]
|
||||
},
|
||||
"schedule": {
|
||||
"nullable": true,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Schedule"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"assignAllTeamMembers": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"bookerLayouts": {
|
||||
"type": "object"
|
||||
},
|
||||
"users": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"type": "object"
|
||||
},
|
||||
"isDynamic": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"title",
|
||||
"description",
|
||||
"slug",
|
||||
"isInstantEvent",
|
||||
"length",
|
||||
"locations",
|
||||
"customInputs",
|
||||
"disableGuests",
|
||||
"metadata",
|
||||
"lockTimeZoneToggleOnBookingPage",
|
||||
"requiresConfirmation",
|
||||
"requiresBookerEmailVerification",
|
||||
"price",
|
||||
"currency",
|
||||
"seatsShowAvailabilityCount",
|
||||
"bookingFields",
|
||||
"workflows",
|
||||
"hosts",
|
||||
"owner",
|
||||
"schedule",
|
||||
"hidden",
|
||||
"assignAllTeamMembers",
|
||||
"users",
|
||||
"entity",
|
||||
"isDynamic"
|
||||
]
|
||||
},
|
||||
"GetEventTypePublicOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"nullable": true,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/PublicEventTypeOutput"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"PublicEventType": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3357,26 +3860,6 @@
|
||||
"credentialId"
|
||||
]
|
||||
},
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"GetBookingsDataEntry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3663,21 +4146,6 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"Location": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"optionValue": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"optionValue",
|
||||
"value"
|
||||
]
|
||||
},
|
||||
"Response": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3757,6 +4225,9 @@
|
||||
},
|
||||
"responses": {
|
||||
"$ref": "#/components/schemas/Response"
|
||||
},
|
||||
"locationUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Hello World
|
||||
@@ -1 +0,0 @@
|
||||
## Cal.com Platform Example (https://platform.cal.com)
|
||||
@@ -1,5 +1,14 @@
|
||||
# @calcom/web
|
||||
|
||||
## 3.9.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @calcom/embed-core@1.4.0
|
||||
- @calcom/embed-react@1.4.0
|
||||
- @calcom/embed-snippet@1.2.0
|
||||
|
||||
## 3.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { GET } from "@calcom/features/tasker/api/cleanup";
|
||||
@@ -0,0 +1 @@
|
||||
export { GET } from "@calcom/features/tasker/api/cron";
|
||||
@@ -33,7 +33,7 @@ interface AppListProps {
|
||||
|
||||
export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppListProps) => {
|
||||
const { data: defaultConferencingApp } = trpc.viewer.getUsersDefaultConferencingApp.useQuery();
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
|
||||
const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>(
|
||||
undefined
|
||||
@@ -139,7 +139,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
||||
...app,
|
||||
credentialOwner: {
|
||||
name: team.name,
|
||||
avatar: team.logo,
|
||||
avatar: team.logoUrl,
|
||||
teamId: team.teamId,
|
||||
credentialId: team.credentialId,
|
||||
readOnly: !team.isAdmin,
|
||||
@@ -201,7 +201,7 @@ function ConnectOrDisconnectIntegrationMenuItem(props: {
|
||||
const { type, credentialId, isGlobal, installed, handleDisconnect, teamId } = props;
|
||||
const { t } = useLocale();
|
||||
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const handleOpenChange = () => {
|
||||
utils.viewer.integrations.invalidate();
|
||||
};
|
||||
|
||||
@@ -192,7 +192,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const onChanged = () =>
|
||||
Promise.allSettled([
|
||||
utils.viewer.integrations.invalidate(
|
||||
|
||||
@@ -2,7 +2,7 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { doesAppSupportTeamInstall } from "@calcom/app-store/utils";
|
||||
import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner";
|
||||
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import type { AppFrontendPayload } from "@calcom/types/App";
|
||||
@@ -124,8 +124,8 @@ export const InstallAppButtonChild = ({
|
||||
disabled={isInstalled}
|
||||
CustomStartIcon={
|
||||
<Avatar
|
||||
alt={team.logo || ""}
|
||||
imageSrc={team.logo || `${WEBAPP_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
|
||||
alt={team.logoUrl || ""}
|
||||
imageSrc={getPlaceholderAvatar(team.logoUrl, team.name)} // if no image, use default avatar
|
||||
size="sm"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
t,
|
||||
i18n: { language },
|
||||
} = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
|
||||
|
||||
@@ -24,7 +24,7 @@ interface IRescheduleDialog {
|
||||
|
||||
export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
|
||||
const [chargeError, setChargeError] = useState(false);
|
||||
const chargeCardMutation = trpc.viewer.payments.chargeCard.useMutation({
|
||||
|
||||
@@ -23,7 +23,7 @@ interface IRescheduleDialog {
|
||||
|
||||
export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
|
||||
const [rescheduleReason, setRescheduleReason] = useState("");
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
// credentialIds: team?.credentialId ? [team.credentialId] : [],
|
||||
credentialOwner: {
|
||||
name: team.name,
|
||||
avatar: team.logo,
|
||||
avatar: team.logoUrl,
|
||||
teamId: team.teamId,
|
||||
credentialId: team.credentialId,
|
||||
},
|
||||
|
||||
@@ -480,6 +480,7 @@ export const EventSetupTab = (
|
||||
label={t("title")}
|
||||
{...(isManagedEventType || isChildrenManagedEventType ? titleLockedProps : {})}
|
||||
defaultValue={eventType.title}
|
||||
data-testid="event-title"
|
||||
{...formMethods.register("title")}
|
||||
/>
|
||||
<div>
|
||||
@@ -494,6 +495,7 @@ export const EventSetupTab = (
|
||||
label={t("URL")}
|
||||
{...(isManagedEventType || isChildrenManagedEventType ? urlLockedProps : {})}
|
||||
defaultValue={eventType.slug}
|
||||
data-testid="event-slug"
|
||||
addOnLeading={
|
||||
<>
|
||||
{urlPrefix}/
|
||||
|
||||
@@ -123,7 +123,7 @@ function DeleteDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: { isManagedEvent: string; eventTypeId: number } & Pick<DialogProps, "open" | "onOpenChange">) {
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
@@ -481,7 +481,6 @@ function EventTypeSingleLayout({
|
||||
sticky
|
||||
linkShallow
|
||||
itemClassname="items-start"
|
||||
iconClassName="md:mt-px"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@c
|
||||
export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({ eventTypeId: eventType.id });
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function InstantEventController({
|
||||
|
||||
const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({
|
||||
|
||||
@@ -25,7 +25,7 @@ const UserProfile = () => {
|
||||
|
||||
const { data: eventTypes } = trpc.viewer.eventTypes.list.useQuery();
|
||||
const [imageSrc, setImageSrc] = useState<string>(user?.avatar || "");
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const router = useRouter();
|
||||
const createEventType = trpc.viewer.eventTypes.create.useMutation();
|
||||
const telemetry = useTelemetry();
|
||||
@@ -33,7 +33,7 @@ const UserProfile = () => {
|
||||
|
||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async (_data, context) => {
|
||||
if (context.avatar) {
|
||||
if (context.avatarUrl) {
|
||||
showToast(t("your_user_profile_updated_successfully"), "success");
|
||||
await utils.viewer.me.refetch();
|
||||
} else
|
||||
@@ -74,7 +74,7 @@ const UserProfile = () => {
|
||||
event.preventDefault();
|
||||
const enteredAvatar = avatarRef.current?.value;
|
||||
mutation.mutate({
|
||||
avatar: enteredAvatar,
|
||||
avatarUrl: enteredAvatar,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
telemetry.event(telemetryEventTypes.onboardingStarted);
|
||||
}, [telemetry]);
|
||||
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const onSuccess = async () => {
|
||||
await utils.viewer.me.invalidate();
|
||||
nextStep();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import { Badge, Button, showToast } from "@calcom/ui";
|
||||
|
||||
const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonation: boolean }) => {
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { FormValues } from "@pages/settings/my-account/general";
|
||||
import { useState } from "react";
|
||||
import type { UseFormSetValue } from "react-hook-form";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Button,
|
||||
Label,
|
||||
DateRangePicker,
|
||||
TimezoneSelect,
|
||||
SettingsToggle,
|
||||
DatePicker,
|
||||
} from "@calcom/ui";
|
||||
|
||||
interface TravelScheduleModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: () => void;
|
||||
setValue: UseFormSetValue<FormValues>;
|
||||
existingSchedules: FormValues["travelSchedules"];
|
||||
}
|
||||
|
||||
const TravelScheduleModal = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
setValue,
|
||||
existingSchedules,
|
||||
}: TravelScheduleModalProps) => {
|
||||
const { t } = useLocale();
|
||||
const { timezone: preferredTimezone } = useTimePreferences();
|
||||
|
||||
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||
const [endDate, setEndDate] = useState<Date | undefined>(new Date());
|
||||
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState(preferredTimezone);
|
||||
const [isNoEndDate, setIsNoEndDate] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const isOverlapping = (newSchedule: { startDate: Date; endDate?: Date }) => {
|
||||
const newStart = dayjs(newSchedule.startDate);
|
||||
const newEnd = newSchedule.endDate ? dayjs(newSchedule.endDate) : null;
|
||||
|
||||
for (const schedule of existingSchedules) {
|
||||
const start = dayjs(schedule.startDate);
|
||||
const end = schedule.endDate ? dayjs(schedule.endDate) : null;
|
||||
|
||||
if (!newEnd) {
|
||||
// if the start date is after or on the existing schedule's start date and before the existing schedule's end date (if it has one)
|
||||
if (newStart.isSame(start) || newStart.isAfter(start)) {
|
||||
if (!end || newStart.isSame(end) || newStart.isBefore(end)) return true;
|
||||
}
|
||||
} else {
|
||||
// For schedules with an end date, check for any overlap
|
||||
if (newStart.isSame(end) || newStart.isBefore(end) || end === null) {
|
||||
if (newEnd.isSame(start) || newEnd.isAfter(start)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetValues = () => {
|
||||
setStartDate(new Date());
|
||||
setEndDate(new Date());
|
||||
setSelectedTimeZone(preferredTimezone);
|
||||
setIsNoEndDate(false);
|
||||
};
|
||||
|
||||
const createNewSchedule = () => {
|
||||
const newSchedule = {
|
||||
startDate,
|
||||
endDate,
|
||||
timeZone: selectedTimeZone,
|
||||
};
|
||||
|
||||
if (!isOverlapping(newSchedule)) {
|
||||
setValue("travelSchedules", existingSchedules.concat(newSchedule), { shouldDirty: true });
|
||||
onOpenChange();
|
||||
resetValues();
|
||||
} else {
|
||||
setErrorMessage(t("overlaps_with_existing_schedule"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
title={t("travel_schedule")}
|
||||
description={t("travel_schedule_description")}
|
||||
type="creation">
|
||||
<div>
|
||||
{!isNoEndDate ? (
|
||||
<>
|
||||
<Label className="mt-2">{t("time_range")}</Label>
|
||||
<DateRangePicker
|
||||
startDate={startDate}
|
||||
endDate={endDate ?? startDate}
|
||||
onDatesChange={({ startDate: newStartDate, endDate: newEndDate }) => {
|
||||
setStartDate(newStartDate);
|
||||
setEndDate(newEndDate);
|
||||
setErrorMessage("");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Label className="mt-2">{t("date")}</Label>
|
||||
<DatePicker
|
||||
minDate={new Date()}
|
||||
date={startDate}
|
||||
className="w-56"
|
||||
onDatesChange={(newDate) => {
|
||||
setStartDate(newDate);
|
||||
setErrorMessage("");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="text-error mt-1 text-sm">{errorMessage}</div>
|
||||
<div className="mt-3">
|
||||
<SettingsToggle
|
||||
labelClassName="mt-1 font-normal"
|
||||
title={t("schedule_tz_without_end_date")}
|
||||
checked={isNoEndDate}
|
||||
onCheckedChange={(e) => {
|
||||
setEndDate(!e ? startDate : undefined);
|
||||
setIsNoEndDate(e);
|
||||
setErrorMessage("");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Label className="mt-6">{t("timezone")}</Label>
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
value={selectedTimeZone}
|
||||
onChange={({ value }) => setSelectedTimeZone(value)}
|
||||
className="mb-11 mt-2 w-full rounded-md text-sm"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter showDivider className="relative">
|
||||
<DialogClose />
|
||||
<Button
|
||||
onClick={() => {
|
||||
createNewSchedule();
|
||||
}}>
|
||||
{t("add")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default TravelScheduleModal;
|
||||
+10
-2
@@ -1,3 +1,4 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
@@ -38,7 +39,7 @@ export const OAuthClientCard = ({
|
||||
isLoading,
|
||||
areEmailsEnabled,
|
||||
}: OAuthClientCardProps) => {
|
||||
console.log(areEmailsEnabled);
|
||||
const router = useRouter();
|
||||
|
||||
const clientPermissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value, index) => {
|
||||
let permissionsMessage = "";
|
||||
@@ -139,7 +140,14 @@ export const OAuthClientCard = ({
|
||||
<span className="text-sm font-semibold">Emails enabled:</span> {areEmailsEnabled ? "Yes" : "No"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<div className="flex items-start gap-4">
|
||||
<Button
|
||||
className="bg-subtle hover:bg-emphasis text-white"
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
onClick={() => router.push(`/settings/organizations/platform/oauth-clients/create?clientId=${id}`)}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-red-500 text-white hover:bg-red-600"
|
||||
loading={isLoading}
|
||||
|
||||
+105
-22
@@ -1,6 +1,6 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { FC } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { useForm, useFieldArray } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -8,7 +8,24 @@ import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions"
|
||||
import { showToast } from "@calcom/ui";
|
||||
import { Meta, Button, TextField, Label, Tooltip } from "@calcom/ui";
|
||||
|
||||
import { useCreateOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient";
|
||||
import { useOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/useOAuthClients";
|
||||
import {
|
||||
useCreateOAuthClient,
|
||||
useUpdateOAuthClient,
|
||||
} from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient";
|
||||
|
||||
import {
|
||||
hasAppsReadPermission,
|
||||
hasAppsWritePermission,
|
||||
hasBookingReadPermission,
|
||||
hasBookingWritePermission,
|
||||
hasEventTypeReadPermission,
|
||||
hasEventTypeWritePermission,
|
||||
hasProfileReadPermission,
|
||||
hasProfileWritePermission,
|
||||
hasScheduleReadPermission,
|
||||
hasScheduleWritePermission,
|
||||
} from "../../../../../../../packages/platform/utils/permissions";
|
||||
|
||||
type FormValues = {
|
||||
name: string;
|
||||
@@ -33,9 +50,10 @@ type FormValues = {
|
||||
areEmailsEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const OAuthClientForm: FC = () => {
|
||||
export const OAuthClientForm: FC<{ clientId?: string }> = ({ clientId }) => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const { data, isFetched, isError, refetch } = useOAuthClient(clientId);
|
||||
const { register, control, handleSubmit, setValue } = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
redirectUris: [{ uri: "" }],
|
||||
@@ -45,6 +63,32 @@ export const OAuthClientForm: FC = () => {
|
||||
control,
|
||||
name: "redirectUris",
|
||||
});
|
||||
useEffect(() => {
|
||||
if (isFetched && data && !isError) {
|
||||
setValue("name", data.name);
|
||||
data.bookingRedirectUri && setValue("bookingRedirectUri", data.bookingRedirectUri);
|
||||
data.bookingCancelRedirectUri && setValue("bookingCancelRedirectUri", data.bookingCancelRedirectUri);
|
||||
data.bookingRescheduleRedirectUri &&
|
||||
setValue("bookingRescheduleRedirectUri", data.bookingRescheduleRedirectUri);
|
||||
setValue("areEmailsEnabled", data?.areEmailsEnabled);
|
||||
data?.redirectUris.forEach((uri: string, index: number) => {
|
||||
index === 0 && setValue(`redirectUris.${index}.uri`, uri);
|
||||
index !== 0 && append({ uri });
|
||||
});
|
||||
if (hasAppsReadPermission(data.permissions)) setValue("appsRead", true);
|
||||
if (hasAppsWritePermission(data.permissions)) setValue("appsWrite", true);
|
||||
if (hasBookingReadPermission(data.permissions)) setValue("bookingRead", true);
|
||||
if (hasBookingWritePermission(data.permissions)) setValue("bookingWrite", true);
|
||||
if (hasEventTypeReadPermission(data.permissions)) setValue("eventTypeRead", true);
|
||||
if (hasEventTypeWritePermission(data.permissions)) setValue("eventTypeWrite", true);
|
||||
if (hasProfileReadPermission(data.permissions)) setValue("profileRead", true);
|
||||
if (hasProfileWritePermission(data.permissions)) setValue("profileWrite", true);
|
||||
if (hasScheduleReadPermission(data.permissions)) setValue("scheduleRead", true);
|
||||
if (hasScheduleWritePermission(data.permissions)) setValue("scheduleWrite", true);
|
||||
}
|
||||
}, [isFetched, data]);
|
||||
const disabledForm = Boolean(clientId && !isFetched && isError);
|
||||
|
||||
const [isSelectAllPermissionsChecked, setIsSelectAllPermissionsChecked] = useState(false);
|
||||
|
||||
const selectAllPermissions = useCallback(() => {
|
||||
@@ -59,15 +103,27 @@ export const OAuthClientForm: FC = () => {
|
||||
setIsSelectAllPermissionsChecked((preValue) => !preValue);
|
||||
}, [isSelectAllPermissionsChecked, setValue]);
|
||||
|
||||
const { mutateAsync, isPending } = useCreateOAuthClient({
|
||||
const { mutateAsync: save, isPending: isSaving } = useCreateOAuthClient({
|
||||
onSuccess: () => {
|
||||
showToast("OAuth client created successfully", "success");
|
||||
refetch();
|
||||
router.push("/settings/organizations/platform/oauth-clients");
|
||||
},
|
||||
onError: () => {
|
||||
showToast("Internal server error, please try again later", "error");
|
||||
},
|
||||
});
|
||||
const { mutateAsync: update, isPending: isUpdating } = useUpdateOAuthClient({
|
||||
onSuccess: () => {
|
||||
showToast("OAuth client updated successfully", "success");
|
||||
refetch();
|
||||
router.push("/settings/organizations/platform/oauth-clients");
|
||||
},
|
||||
onError: () => {
|
||||
showToast("Internal server error, please try again later", "error");
|
||||
},
|
||||
clientId,
|
||||
});
|
||||
|
||||
const onSubmit = (data: FormValues) => {
|
||||
let userPermissions = 0;
|
||||
@@ -82,17 +138,31 @@ export const OAuthClientForm: FC = () => {
|
||||
if (data[`${entityKey}Write`]) userPermissions |= write;
|
||||
});
|
||||
|
||||
mutateAsync({
|
||||
name: data.name,
|
||||
permissions: userPermissions,
|
||||
// logo: data.logo,
|
||||
redirectUris: userRedirectUris,
|
||||
bookingRedirectUri: data.bookingRedirectUri,
|
||||
bookingCancelRedirectUri: data.bookingCancelRedirectUri,
|
||||
bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri,
|
||||
areEmailsEnabled: data.areEmailsEnabled,
|
||||
});
|
||||
if (clientId) {
|
||||
// don't update permissions if client is already created
|
||||
update({
|
||||
name: data.name,
|
||||
// logo: data.logo,
|
||||
redirectUris: userRedirectUris,
|
||||
bookingRedirectUri: data.bookingRedirectUri,
|
||||
bookingCancelRedirectUri: data.bookingCancelRedirectUri,
|
||||
bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri,
|
||||
areEmailsEnabled: data.areEmailsEnabled,
|
||||
});
|
||||
} else {
|
||||
save({
|
||||
name: data.name,
|
||||
permissions: userPermissions,
|
||||
// logo: data.logo,
|
||||
redirectUris: userRedirectUris,
|
||||
bookingRedirectUri: data.bookingRedirectUri,
|
||||
bookingCancelRedirectUri: data.bookingCancelRedirectUri,
|
||||
bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri,
|
||||
areEmailsEnabled: data.areEmailsEnabled,
|
||||
});
|
||||
}
|
||||
};
|
||||
const isPending = isSaving || isUpdating;
|
||||
|
||||
const permissionsCheckboxes = Object.keys(PERMISSIONS_GROUPED_MAP).map((key) => {
|
||||
const entity = key as keyof typeof PERMISSIONS_GROUPED_MAP;
|
||||
@@ -109,6 +179,7 @@ export const OAuthClientForm: FC = () => {
|
||||
id={`${permissionKey}Read`}
|
||||
className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed"
|
||||
type="checkbox"
|
||||
disabled={Boolean(clientId)}
|
||||
/>
|
||||
<label htmlFor={`${permissionKey}Read`} className="cursor-pointer text-sm">
|
||||
Read
|
||||
@@ -120,6 +191,7 @@ export const OAuthClientForm: FC = () => {
|
||||
id={`${permissionKey}Write`}
|
||||
className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed"
|
||||
type="checkbox"
|
||||
disabled={Boolean(clientId)}
|
||||
/>
|
||||
<label htmlFor={`${permissionKey}Write`} className="cursor-pointer text-sm">
|
||||
Write
|
||||
@@ -133,15 +205,15 @@ export const OAuthClientForm: FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<Meta
|
||||
title={t("oauth_form_title")}
|
||||
description={t("oauth_form_description")}
|
||||
title={t("oauth_form_title") + (clientId ? " - Update" : "")}
|
||||
description={t("oauth_form_description") + (clientId ? " - Update" : "")}
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
<form
|
||||
className="border-subtle rounded-b-lg border border-t-0 px-4 pb-8 pt-2"
|
||||
onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="mt-6">
|
||||
<TextField required={true} label="Client name" {...register("name")} />
|
||||
<TextField disabled={disabledForm} required={true} label="Client name" {...register("name")} />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Label>Redirect uris</Label>
|
||||
@@ -154,6 +226,7 @@ export const OAuthClientForm: FC = () => {
|
||||
required={index === 0}
|
||||
className="w-[100%]"
|
||||
label=""
|
||||
disabled={disabledForm}
|
||||
{...register(`redirectUris.${index}.uri` as const)}
|
||||
/>
|
||||
</div>
|
||||
@@ -168,6 +241,7 @@ export const OAuthClientForm: FC = () => {
|
||||
onClick={() => {
|
||||
append({ uri: "" });
|
||||
}}
|
||||
disabled={disabledForm}
|
||||
/>
|
||||
{index > 0 && (
|
||||
<Button
|
||||
@@ -180,6 +254,7 @@ export const OAuthClientForm: FC = () => {
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
}}
|
||||
disabled={disabledForm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -224,6 +299,7 @@ export const OAuthClientForm: FC = () => {
|
||||
label="Booking redirect uri"
|
||||
className="w-[100%]"
|
||||
{...register("bookingRedirectUri")}
|
||||
disabled={disabledForm}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -234,6 +310,7 @@ export const OAuthClientForm: FC = () => {
|
||||
label="Booking cancel redirect uri"
|
||||
className="w-[100%]"
|
||||
{...register("bookingCancelRedirectUri")}
|
||||
disabled={disabledForm}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -244,6 +321,7 @@ export const OAuthClientForm: FC = () => {
|
||||
label="Booking reschedule redirect uri"
|
||||
className="w-[100%]"
|
||||
{...register("bookingRescheduleRedirectUri")}
|
||||
disabled={disabledForm}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -253,22 +331,27 @@ export const OAuthClientForm: FC = () => {
|
||||
id="areEmailsEnabled"
|
||||
className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed"
|
||||
type="checkbox"
|
||||
disabled={disabledForm}
|
||||
/>
|
||||
<label htmlFor="areEmailsEnabled" className="cursor-pointer px-2 text-base font-semibold">
|
||||
Enable emails
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="flex justify-between">
|
||||
<h1 className="text-base font-semibold underline">Permissions</h1>
|
||||
<Button type="button" onClick={selectAllPermissions}>
|
||||
<Tooltip side="right" content="Permissions once set cannot be modified">
|
||||
<h1 className="text-base font-semibold underline">Permissions</h1>
|
||||
</Tooltip>
|
||||
<Button type="button" onClick={selectAllPermissions} disabled={disabledForm || Boolean(clientId)}>
|
||||
{!isSelectAllPermissionsChecked ? "Select all" : "Discard all"}
|
||||
</Button>
|
||||
</div>
|
||||
<div>{permissionsCheckboxes}</div>
|
||||
</div>
|
||||
<Button className="mt-6" type="submit" loading={isPending}>
|
||||
Submit
|
||||
|
||||
<Button className="mt-6" type="submit" loading={isPending} disabled={disabledForm}>
|
||||
{clientId ? "Update" : "Submit"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -72,6 +72,7 @@ export const getEventTypesFromDB = async (id: number) => {
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
schedulingType: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
},
|
||||
|
||||
@@ -52,7 +52,6 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
});
|
||||
|
||||
let name: string;
|
||||
let isAway = false;
|
||||
let hideBranding = false;
|
||||
|
||||
const notFound = {
|
||||
@@ -96,7 +95,6 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
}
|
||||
|
||||
name = username;
|
||||
isAway = user.away;
|
||||
hideBranding = user.hideBranding;
|
||||
}
|
||||
|
||||
@@ -129,7 +127,6 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
eventData.length
|
||||
),
|
||||
booking,
|
||||
away: isAway,
|
||||
user: name,
|
||||
slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
|
||||
@@ -33,7 +33,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
logo: true,
|
||||
logoUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+21
-3
@@ -17,20 +17,38 @@ export const useOAuthClients = () => {
|
||||
return { ...query, data: query.data?.data ?? [] };
|
||||
};
|
||||
|
||||
export const useOAuthClient = (clientId: string) => {
|
||||
export const useOAuthClient = (clientId?: string) => {
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
data: response,
|
||||
isFetched,
|
||||
isError,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
isFetchedAfterMount,
|
||||
refetch,
|
||||
} = useQuery<ApiSuccessResponse<PlatformOAuthClient>>({
|
||||
queryKey: ["oauth-client"],
|
||||
queryKey: ["oauth-client", clientId],
|
||||
queryFn: () => {
|
||||
return fetch(`/api/v2/oauth-clients/${clientId}`, {
|
||||
method: "get",
|
||||
headers: { "Content-type": "application/json" },
|
||||
}).then((res) => res.json());
|
||||
},
|
||||
enabled: clientId !== undefined,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
return { isLoading, error, data: response?.data };
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
data: response?.data,
|
||||
isFetched,
|
||||
isError,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
isFetchedAfterMount,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
+38
-1
@@ -46,6 +46,43 @@ export const useCreateOAuthClient = (
|
||||
return mutation;
|
||||
};
|
||||
|
||||
export const useUpdateOAuthClient = (
|
||||
{ onSuccess, onError, clientId }: IPersistOAuthClient & { clientId?: string } = {
|
||||
onSuccess: () => {
|
||||
return;
|
||||
},
|
||||
onError: () => {
|
||||
return;
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const mutation = useMutation<
|
||||
ApiResponse<{ clientId: string; clientSecret: string }>,
|
||||
unknown,
|
||||
Omit<CreateOAuthClientInput, "permissions">
|
||||
>({
|
||||
mutationFn: (data) => {
|
||||
return fetch(`/api/v2/oauth-clients/${clientId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
}).then((res) => res?.json());
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data.status === SUCCESS_STATUS) {
|
||||
onSuccess?.();
|
||||
} else {
|
||||
onError?.();
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
onError?.();
|
||||
},
|
||||
});
|
||||
|
||||
return mutation;
|
||||
};
|
||||
|
||||
export const useDeleteOAuthClient = (
|
||||
{ onSuccess, onError }: IPersistOAuthClient = {
|
||||
onSuccess: () => {
|
||||
@@ -62,7 +99,7 @@ export const useDeleteOAuthClient = (
|
||||
return fetch(`/api/v2/oauth-clients/${id}`, {
|
||||
method: "delete",
|
||||
headers: { "Content-type": "application/json" },
|
||||
}).then((res) => res.json());
|
||||
}).then((res) => res?.json());
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data.status === SUCCESS_STATUS) {
|
||||
|
||||
@@ -53,7 +53,10 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
|
||||
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || signupDisabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `/auth/error?error=Signup is disabled in this instance`,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
@@ -98,7 +101,10 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
|
||||
if (!verificationToken || verificationToken.expires < new Date()) {
|
||||
return {
|
||||
notFound: true,
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `/auth/error?error=Verification Token is missing or has expired`,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
metadata: eventData.metadata,
|
||||
},
|
||||
booking,
|
||||
away: false,
|
||||
user: teamSlug,
|
||||
teamId: team.id,
|
||||
slug: meetingSlug,
|
||||
|
||||
@@ -98,6 +98,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
isPrivate: true,
|
||||
isOrganization: true,
|
||||
metadata: true,
|
||||
logoUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -213,6 +214,9 @@ function getTeamWithoutMetadata<T extends Pick<Team, "metadata">>(team: T) {
|
||||
const teamMetadata = teamMetadataSchema.parse(metadata);
|
||||
return {
|
||||
...rest,
|
||||
requestedSlug: teamMetadata?.requestedSlug,
|
||||
// add requestedSlug if available.
|
||||
...(typeof teamMetadata?.requestedSlug !== "undefined"
|
||||
? { requestedSlug: teamMetadata?.requestedSlug }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,3 +16,12 @@ export type WorkingHours = {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
|
||||
export type TravelSchedule = {
|
||||
id: number;
|
||||
timeZone: string;
|
||||
userId: number;
|
||||
startDate: Date;
|
||||
endDate: Date | null;
|
||||
prevTimeZone: string | null;
|
||||
};
|
||||
|
||||
@@ -44,11 +44,12 @@ import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
EmailInput,
|
||||
@@ -225,6 +226,7 @@ export default function Success(props: PageProps) {
|
||||
|
||||
const giphyAppData = getEventTypeAppData(eventType, "giphy");
|
||||
const giphyImage = giphyAppData?.thankYouPage;
|
||||
const isRoundRobin = eventType.schedulingType === SchedulingType.ROUND_ROBIN;
|
||||
|
||||
const eventName = getEventName(eventNameObject, true);
|
||||
// Confirmation can be needed in two cases as of now
|
||||
@@ -294,6 +296,7 @@ export default function Success(props: PageProps) {
|
||||
|
||||
function getTitle(): string {
|
||||
const titleSuffix = props.recurringBookings ? "_recurring" : "";
|
||||
const titlePrefix = isRoundRobin ? "round_robin_" : "";
|
||||
if (isCancelled) {
|
||||
return "";
|
||||
}
|
||||
@@ -305,6 +308,11 @@ export default function Success(props: PageProps) {
|
||||
}
|
||||
return t(`needs_to_be_confirmed_or_rejected${titleSuffix}`);
|
||||
}
|
||||
if (bookingInfo.user) {
|
||||
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
|
||||
user: bookingInfo.user.name || bookingInfo.user.email,
|
||||
});
|
||||
}
|
||||
return t(`emailed_you_and_attendees${titleSuffix}`);
|
||||
}
|
||||
|
||||
@@ -396,27 +404,36 @@ export default function Success(props: PageProps) {
|
||||
{!isFeedbackMode && (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
"mx-auto flex items-center justify-center",
|
||||
!giphyImage && !isCancelled && !needsConfirmation
|
||||
? "bg-success h-12 w-12 rounded-full"
|
||||
: "",
|
||||
!giphyImage && !isCancelled && needsConfirmation
|
||||
? "bg-subtle h-12 w-12 rounded-full"
|
||||
: "",
|
||||
isCancelled ? "bg-error h-12 w-12 rounded-full " : ""
|
||||
)}>
|
||||
{giphyImage && !needsConfirmation && !isCancelled && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={giphyImage} alt="Gif from Giphy" />
|
||||
className={classNames(isRoundRobin && "min-w-32 min-h-24 relative mx-auto h-24 w-32")}>
|
||||
{isRoundRobin && bookingInfo.user && (
|
||||
<Avatar
|
||||
className="mx-auto flex items-center justify-center"
|
||||
alt={bookingInfo.user.name || bookingInfo.user.email}
|
||||
size="xl"
|
||||
imageSrc={`${bookingInfo.user.avatarUrl}`}
|
||||
/>
|
||||
)}
|
||||
{!giphyImage && !needsConfirmation && !isCancelled && (
|
||||
<Icon name="check" className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
)}
|
||||
{needsConfirmation && !isCancelled && (
|
||||
<Icon name="calendar" className="text-emphasis h-5 w-5" />
|
||||
)}
|
||||
{isCancelled && <Icon name="x" className="h-5 w-5 text-red-600 dark:text-red-200" />}
|
||||
<div
|
||||
className={classNames(
|
||||
"mx-auto flex h-12 w-12 items-center justify-center rounded-full",
|
||||
isRoundRobin &&
|
||||
"border-cal-bg dark:border-cal-bg-muted absolute bottom-0 right-0 z-10 h-12 w-12 border-8",
|
||||
!giphyImage && !isCancelled && !needsConfirmation ? "bg-success" : "",
|
||||
!giphyImage && !isCancelled && needsConfirmation ? "bg-subtle" : "",
|
||||
isCancelled ? "bg-error" : ""
|
||||
)}>
|
||||
{giphyImage && !needsConfirmation && !isCancelled && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={giphyImage} alt="Gif from Giphy" />
|
||||
)}
|
||||
{!giphyImage && !needsConfirmation && !isCancelled && (
|
||||
<Icon name="check" className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
)}
|
||||
{needsConfirmation && !isCancelled && (
|
||||
<Icon name="calendar" className="text-emphasis h-5 w-5" />
|
||||
)}
|
||||
{isCancelled && <Icon name="x" className="h-5 w-5 text-red-600 dark:text-red-200" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 mt-6 text-center last:mb-0">
|
||||
<h3
|
||||
@@ -463,7 +480,7 @@ export default function Success(props: PageProps) {
|
||||
)}
|
||||
<div className="font-medium">{t("what")}</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0" data-testid="booking-title">
|
||||
{eventName}
|
||||
{isRoundRobin ? bookingInfo.title : eventName}
|
||||
</div>
|
||||
<div className="font-medium">{t("when")}</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0">
|
||||
@@ -860,10 +877,10 @@ export default function Success(props: PageProps) {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="my-3 flex justify-end">
|
||||
<div className="my-3 flex justify-center space-x-1">
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
|
||||
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
|
||||
rateValue === 1
|
||||
? "border-focus bg-emphasis"
|
||||
: "border-muted bg-default opacity-50"
|
||||
@@ -874,7 +891,7 @@ export default function Success(props: PageProps) {
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
|
||||
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
|
||||
rateValue === 2
|
||||
? "border-focus bg-emphasis"
|
||||
: "border-muted bg-default opacity-50"
|
||||
@@ -885,7 +902,7 @@ export default function Success(props: PageProps) {
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
|
||||
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
|
||||
rateValue === 3
|
||||
? "border-focus bg-emphasis"
|
||||
: " border-muted bg-default opacity-50"
|
||||
@@ -896,7 +913,7 @@ export default function Success(props: PageProps) {
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
|
||||
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
|
||||
rateValue === 4
|
||||
? "border-focus bg-emphasis"
|
||||
: "border-muted bg-default opacity-50"
|
||||
@@ -907,7 +924,7 @@ export default function Success(props: PageProps) {
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
|
||||
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
|
||||
rateValue === 5
|
||||
? "border-focus bg-emphasis"
|
||||
: "border-muted bg-default opacity-50"
|
||||
|
||||
@@ -230,7 +230,7 @@ export const EventTypeList = ({
|
||||
const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = useState<SchedulingType | null>(
|
||||
null
|
||||
);
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const mutation = trpc.viewer.eventTypeOrder.useMutation({
|
||||
onError: async (err) => {
|
||||
console.error(err.message);
|
||||
@@ -896,7 +896,7 @@ const Main = ({
|
||||
className="mt-4 flex flex-col"
|
||||
data-testid={`slug-${group.profile.slug}`}
|
||||
key={group.profile.slug}>
|
||||
{/* If the group is readonly and empty don't leave a floating header when the user cant see the create box due
|
||||
{/* If the group is readonly and empty don't leave a floating header when the user cant see the create box due
|
||||
to it being readonly for that user */}
|
||||
{group.eventTypes.length === 0 && group.metadata.readOnly ? null : (
|
||||
<EventTypeListHeading
|
||||
|
||||
@@ -34,11 +34,26 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
} as const;
|
||||
return redirect;
|
||||
}
|
||||
|
||||
await ssr.viewer.eventTypes.get.prefetch({ id: typeParam });
|
||||
|
||||
const { eventType } = await ssr.viewer.eventTypes.get.fetch({ id: typeParam });
|
||||
|
||||
const getEventTypeById = async (eventTypeId: number) => {
|
||||
await ssr.viewer.eventTypes.get.prefetch({ id: eventTypeId });
|
||||
try {
|
||||
const { eventType } = await ssr.viewer.eventTypes.get.fetch({ id: eventTypeId });
|
||||
return eventType;
|
||||
} catch (e: unknown) {
|
||||
// reject, user has no access to this event type.
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const eventType = await getEventTypeById(typeParam);
|
||||
if (!eventType) {
|
||||
const redirect = {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/event-types",
|
||||
},
|
||||
} as const;
|
||||
return redirect;
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
eventType,
|
||||
|
||||
@@ -141,7 +141,7 @@ export type EventTypeSetup = RouterOutputs["viewer"]["eventTypes"]["get"]["event
|
||||
|
||||
const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const telemetry = useTelemetry();
|
||||
const {
|
||||
data: { tabName },
|
||||
@@ -767,7 +767,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
}, {});
|
||||
|
||||
if (dirtyFieldExists) {
|
||||
updateMutation.mutate({ ...filteredPayload, id: eventType.id });
|
||||
updateMutation.mutate({ ...filteredPayload, id: eventType.id, hashedLink: values.hashedLink });
|
||||
}
|
||||
}}>
|
||||
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||
|
||||
@@ -39,13 +39,14 @@ export type UserPageProps = {
|
||||
allowSEOIndexing: boolean;
|
||||
username: string | null;
|
||||
};
|
||||
users: (Pick<User, "away" | "name" | "username" | "bio" | "verified" | "avatarUrl"> & {
|
||||
users: (Pick<User, "name" | "username" | "bio" | "verified" | "avatarUrl"> & {
|
||||
profile: UserProfile;
|
||||
})[];
|
||||
themeBasis: string | null;
|
||||
markdownStrippedBio: string;
|
||||
safeBio: string;
|
||||
entity: {
|
||||
logoUrl?: string | null;
|
||||
considerUnpublished: boolean;
|
||||
orgSlug?: string | null;
|
||||
name?: string | null;
|
||||
@@ -98,12 +99,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
|
||||
});
|
||||
|
||||
const usersWithoutAvatar = usersInOrgContext.map((user) => {
|
||||
const { avatar: _1, ...rest } = user;
|
||||
return rest;
|
||||
});
|
||||
|
||||
const isDynamicGroup = usersWithoutAvatar.length > 1;
|
||||
const isDynamicGroup = usersInOrgContext.length > 1;
|
||||
log.debug(safeStringify({ usersInOrgContext, isValidOrgDomain, currentOrgDomain, isDynamicGroup }));
|
||||
|
||||
if (isDynamicGroup) {
|
||||
@@ -114,40 +110,27 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
permanent: false,
|
||||
destination: destinationUrl,
|
||||
},
|
||||
} as {
|
||||
redirect: {
|
||||
permanent: false;
|
||||
destination: string;
|
||||
};
|
||||
};
|
||||
} as const;
|
||||
}
|
||||
|
||||
const users = usersWithoutAvatar.map((user) => ({
|
||||
...user,
|
||||
avatar: `/${user.username}/avatar.png`,
|
||||
}));
|
||||
|
||||
const isNonOrgUser = (user: { profile: UserProfile }) => {
|
||||
return !user.profile?.organization;
|
||||
};
|
||||
|
||||
const isThereAnyNonOrgUser = users.some(isNonOrgUser);
|
||||
const isThereAnyNonOrgUser = usersInOrgContext.some(isNonOrgUser);
|
||||
|
||||
if (!users.length || (!isValidOrgDomain && !isThereAnyNonOrgUser)) {
|
||||
if (!usersInOrgContext.length || (!isValidOrgDomain && !isThereAnyNonOrgUser)) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as {
|
||||
notFound: true;
|
||||
};
|
||||
} as const;
|
||||
}
|
||||
|
||||
const [user] = users; //to be used when dealing with single user, not dynamic group
|
||||
const [user] = usersInOrgContext; //to be used when dealing with single user, not dynamic group
|
||||
|
||||
const profile = {
|
||||
name: user.name || user.username || "",
|
||||
image: getUserAvatarUrl({
|
||||
...user,
|
||||
profile: user.profile,
|
||||
avatarUrl: user.avatarUrl,
|
||||
}),
|
||||
theme: user.theme,
|
||||
brandColor: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||
@@ -183,20 +166,20 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||
|
||||
const markdownStrippedBio = stripMarkdown(user?.bio || "");
|
||||
const org = usersWithoutAvatar[0].profile.organization;
|
||||
const org = usersInOrgContext[0].profile.organization;
|
||||
|
||||
return {
|
||||
props: {
|
||||
users: users.map((user) => ({
|
||||
users: usersInOrgContext.map((user) => ({
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
bio: user.bio,
|
||||
avatarUrl: user.avatarUrl,
|
||||
verified: user.verified,
|
||||
profile: user.profile,
|
||||
away: user.away,
|
||||
})),
|
||||
entity: {
|
||||
...(org?.logoUrl ? { logoUrl: org?.logoUrl } : {}),
|
||||
considerUnpublished: !isARedirectFromNonOrgLink && org?.slug === null,
|
||||
orgSlug: currentOrgDomain,
|
||||
name: org?.name ?? null,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import classNames from "classnames";
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import {
|
||||
@@ -23,7 +22,6 @@ import { type getServerSideProps } from "./users-public-view.getServerSideProps"
|
||||
|
||||
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [user] = users; //To be used when we only have a single user, not dynamic group
|
||||
useTheme(profile.theme);
|
||||
@@ -43,8 +41,6 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
...query
|
||||
} = useRouterQuery();
|
||||
|
||||
const isRedirect = searchParams?.get("redirected") === "true" || false;
|
||||
const fromUserNameRedirected = searchParams?.get("username") || "";
|
||||
/*
|
||||
const telemetry = useTelemetry();
|
||||
useEffect(() => {
|
||||
@@ -102,13 +98,13 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
{!isOrg && user.verified && (
|
||||
<Icon
|
||||
name="badge-check"
|
||||
className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
|
||||
className="mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
|
||||
/>
|
||||
)}
|
||||
{isOrg && (
|
||||
<Icon
|
||||
name="badge-check"
|
||||
className=" mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
|
||||
className="mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
@@ -125,47 +121,38 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
<div
|
||||
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
|
||||
data-testid="event-types">
|
||||
{user.away ? (
|
||||
<div className="overflow-hidden rounded-sm border ">
|
||||
<div className="text-muted p-8 text-center">
|
||||
<h2 className="font-cal text-default mb-2 text-3xl">😴{` ${t("user_away")}`}</h2>
|
||||
<p className="mx-auto max-w-md">{t("user_away_description") as string}</p>
|
||||
{eventTypes.map((type) => (
|
||||
<div
|
||||
key={type.id}
|
||||
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">
|
||||
<Icon
|
||||
name="arrow-right"
|
||||
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
||||
<div className="block w-full p-5">
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.profile.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
passHref
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
data-testid="event-type-link">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<h2 className="text-default pr-2 text-sm font-semibold">{type.title}</h2>
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
eventTypes.map((type) => (
|
||||
<div
|
||||
key={type.id}
|
||||
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">
|
||||
<Icon
|
||||
name="arrow-right"
|
||||
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
||||
<div className="block w-full p-5">
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.profile.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
passHref
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
data-testid="event-type-link">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<h2 className=" text-default pr-2 text-sm font-semibold">{type.title}</h2>
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
|
||||
|
||||
@@ -86,7 +86,6 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
booking,
|
||||
user: usernames.join("+"),
|
||||
slug,
|
||||
away: false,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: false,
|
||||
isSEOIndexable: true,
|
||||
@@ -165,7 +164,6 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
},
|
||||
user: username,
|
||||
slug,
|
||||
away: false,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
isSEOIndexable: user?.allowSEOIndexing,
|
||||
|
||||
@@ -23,7 +23,6 @@ export default function Type({
|
||||
user,
|
||||
isEmbed,
|
||||
booking,
|
||||
away,
|
||||
isBrandingHidden,
|
||||
isSEOIndexable,
|
||||
rescheduleUid,
|
||||
@@ -47,7 +46,6 @@ export default function Type({
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
bookingData={booking}
|
||||
isAway={away}
|
||||
hideBranding={isBrandingHidden}
|
||||
entity={eventData.entity}
|
||||
durationConfig={eventData.metadata?.multipleDuration}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useTranscription } from "@daily-co/daily-react";
|
||||
import { useDaily, useDailyEvent } from "@daily-co/daily-react";
|
||||
import React, { Fragment, useCallback, useRef, useState, useLayoutEffect, useEffect } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { showToast } from "@calcom/ui";
|
||||
|
||||
export const CalAiTransctibe = () => {
|
||||
const daily = useDaily();
|
||||
const { t } = useLocale();
|
||||
|
||||
const [transcript, setTranscript] = useState("");
|
||||
|
||||
const [transcriptHeight, setTranscriptHeight] = useState(0);
|
||||
const transcriptRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const transcription = useTranscription();
|
||||
|
||||
useDailyEvent(
|
||||
"app-message",
|
||||
useCallback((ev) => {
|
||||
const data = ev?.data;
|
||||
if (data.user_name && data.text) setTranscript(`${data.user_name}: ${data.text}`);
|
||||
}, [])
|
||||
);
|
||||
|
||||
useDailyEvent("transcription-started", (ev) => {
|
||||
showToast(t("transcription_enabled"), "success");
|
||||
});
|
||||
|
||||
useDailyEvent("transcription-stopped", (ev) => {
|
||||
showToast(t("transcription_stopped"), "success");
|
||||
});
|
||||
|
||||
useDailyEvent("custom-button-click", (ev) => {
|
||||
if (ev?.button_id !== "transcription") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transcription?.isTranscribing) {
|
||||
daily?.stopTranscription();
|
||||
} else {
|
||||
daily?.startTranscription();
|
||||
}
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
setTranscriptHeight(entry.target.scrollHeight);
|
||||
}
|
||||
});
|
||||
|
||||
if (transcriptRef.current) {
|
||||
observer.observe(transcriptRef.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
transcriptRef.current?.scrollTo({
|
||||
top: transcriptRef.current?.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}, [transcriptHeight]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster position="bottom-right" />
|
||||
<div
|
||||
id="cal-ai-thing"
|
||||
ref={transcriptRef}
|
||||
className="max-h-full overflow-x-hidden overflow-y-scroll p-2 text-center text-white">
|
||||
{transcript
|
||||
? transcript.split("\n").map((line, i) => (
|
||||
<Fragment key={`transcript-${i}`}>
|
||||
{i > 0 && <br />}
|
||||
{line}
|
||||
</Fragment>
|
||||
))
|
||||
: ""}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import type { DailyCall } from "@daily-co/daily-js";
|
||||
import DailyIframe from "@daily-co/daily-js";
|
||||
import { DailyProvider } from "@daily-co/daily-react";
|
||||
import Head from "next/head";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-fns";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import { CalAiTransctibe } from "~/videos/ai/ai-transcribe";
|
||||
|
||||
import { type PageProps } from "./videos-single-view.getServerSideProps";
|
||||
|
||||
export default function JoinCall(props: PageProps) {
|
||||
const { t } = useLocale();
|
||||
const { meetingUrl, meetingPassword, booking } = props;
|
||||
const [daily, setDaily] = useState<DailyCall | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const callFrame = DailyIframe.createFrame({
|
||||
@@ -42,8 +47,20 @@ export default function JoinCall(props: PageProps) {
|
||||
},
|
||||
url: meetingUrl,
|
||||
...(typeof meetingPassword === "string" && { token: meetingPassword }),
|
||||
customTrayButtons: {
|
||||
transcription: {
|
||||
label: "Cal.ai",
|
||||
tooltip: "Enable transcription powered by AI",
|
||||
iconPath: `${WEBAPP_URL}/sparkles.svg`,
|
||||
iconPathDarkMode: `${WEBAPP_URL}/sparkles.svg`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setDaily(callFrame);
|
||||
|
||||
callFrame.join();
|
||||
|
||||
return () => {
|
||||
callFrame.destroy();
|
||||
};
|
||||
@@ -67,30 +84,35 @@ export default function JoinCall(props: PageProps) {
|
||||
<meta property="twitter:title" content={`${APP_NAME} Video`} />
|
||||
<meta property="twitter:description" content={t("quick_video_meeting")} />
|
||||
</Head>
|
||||
<div style={{ zIndex: 2, position: "relative" }}>
|
||||
{booking?.user?.organization?.calVideoLogo ? (
|
||||
<img
|
||||
className="min-w-16 min-h-16 fixed z-10 hidden aspect-square h-16 w-16 rounded-full sm:inline-block"
|
||||
src={booking.user.organization.calVideoLogo}
|
||||
alt="My Org Logo"
|
||||
style={{
|
||||
top: 32,
|
||||
left: 32,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className="fixed z-10 hidden sm:inline-block"
|
||||
src={`${WEBSITE_URL}/cal-logo-word-dark.svg`}
|
||||
alt="Logo"
|
||||
style={{
|
||||
top: 32,
|
||||
left: 32,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<VideoMeetingInfo booking={booking} />
|
||||
<DailyProvider callObject={daily}>
|
||||
<div className="mx-auto" style={{ zIndex: 2, position: "absolute", bottom: 60, width: "100%" }}>
|
||||
<CalAiTransctibe />
|
||||
</div>
|
||||
<div style={{ zIndex: 2, position: "relative" }}>
|
||||
{booking?.user?.organization?.calVideoLogo ? (
|
||||
<img
|
||||
className="min-w-16 min-h-16 fixed z-10 hidden aspect-square h-16 w-16 rounded-full sm:inline-block"
|
||||
src={booking.user.organization.calVideoLogo}
|
||||
alt="My Org Logo"
|
||||
style={{
|
||||
top: 32,
|
||||
left: 32,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className="fixed z-10 hidden sm:inline-block"
|
||||
src={`${WEBSITE_URL}/cal-logo-word-dark.svg`}
|
||||
alt="Logo"
|
||||
style={{
|
||||
top: 32,
|
||||
left: 32,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<VideoMeetingInfo booking={booking} />
|
||||
</DailyProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@ const nextConfig = {
|
||||
experimental: {
|
||||
// externalize server-side node_modules with size > 1mb, to improve dev mode performance/RAM usage
|
||||
serverComponentsExternalPackages: ["next-i18next"],
|
||||
optimizePackageImports: ["@calcom/ui"],
|
||||
},
|
||||
i18n: {
|
||||
...i18n,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.9.8",
|
||||
"version": "4.0.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -38,7 +38,8 @@
|
||||
"@calcom/trpc": "*",
|
||||
"@calcom/tsconfig": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@daily-co/daily-js": "^0.37.0",
|
||||
"@daily-co/daily-js": "^0.59.0",
|
||||
"@daily-co/daily-react": "^0.17.2",
|
||||
"@formkit/auto-animate": "1.0.0-beta.5",
|
||||
"@glidejs/glide": "^3.5.2",
|
||||
"@hookform/error-message": "^2.0.0",
|
||||
@@ -121,6 +122,7 @@
|
||||
"react-timezone-select": "^1.4.0",
|
||||
"react-turnstile": "^1.1.3",
|
||||
"react-use-intercom": "1.5.1",
|
||||
"recoil": "^0.7.7",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rrule": "^2.7.1",
|
||||
"sanitize-html": "^2.10.0",
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { getDefaultScheduleId } from "@calcom/trpc/server/routers/viewer/availability/util";
|
||||
|
||||
const travelScheduleSelect = {
|
||||
id: true,
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
timeZone: true,
|
||||
prevTimeZone: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
timeZone: true,
|
||||
defaultScheduleId: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const apiKey = req.headers.authorization || req.query.apiKey;
|
||||
if (process.env.CRON_API_KEY !== apiKey) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "POST") {
|
||||
res.status(405).json({ message: "Invalid method" });
|
||||
return;
|
||||
}
|
||||
|
||||
let timeZonesChanged = 0;
|
||||
|
||||
const setNewTimeZone = async (timeZone: string, user: { id: number; defaultScheduleId: number | null }) => {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
timeZone: timeZone,
|
||||
},
|
||||
});
|
||||
|
||||
const defaultScheduleId = await getDefaultScheduleId(user.id, prisma);
|
||||
|
||||
if (!user.defaultScheduleId) {
|
||||
// set default schedule if not already set
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
defaultScheduleId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.schedule.updateMany({
|
||||
where: {
|
||||
id: defaultScheduleId,
|
||||
},
|
||||
data: {
|
||||
timeZone: timeZone,
|
||||
},
|
||||
});
|
||||
timeZonesChanged++;
|
||||
};
|
||||
|
||||
/* travelSchedules should be deleted automatically when timezone is set back to original tz,
|
||||
but we do this in case there cron job didn't run for some reason
|
||||
*/
|
||||
const schedulesToDelete = await prisma.travelSchedule.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
startDate: {
|
||||
lt: dayjs.utc().subtract(2, "day").toDate(),
|
||||
},
|
||||
endDate: null,
|
||||
},
|
||||
{
|
||||
endDate: {
|
||||
lt: dayjs.utc().subtract(2, "day").toDate(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
select: travelScheduleSelect,
|
||||
});
|
||||
|
||||
for (const travelSchedule of schedulesToDelete) {
|
||||
if (travelSchedule.prevTimeZone) {
|
||||
await setNewTimeZone(travelSchedule.prevTimeZone, travelSchedule.user);
|
||||
}
|
||||
await prisma.travelSchedule.delete({
|
||||
where: {
|
||||
id: travelSchedule.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const travelSchedulesCloseToCurrentDate = await prisma.travelSchedule.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
startDate: {
|
||||
gte: dayjs.utc().subtract(1, "day").toDate(),
|
||||
lte: dayjs.utc().add(1, "day").toDate(),
|
||||
},
|
||||
},
|
||||
{
|
||||
endDate: {
|
||||
gte: dayjs.utc().subtract(1, "day").toDate(),
|
||||
lte: dayjs.utc().add(1, "day").toDate(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
select: travelScheduleSelect,
|
||||
});
|
||||
|
||||
const travelScheduleIdsToDelete = [];
|
||||
|
||||
for (const travelSchedule of travelSchedulesCloseToCurrentDate) {
|
||||
const userTz = travelSchedule.user.timeZone;
|
||||
const offset = dayjs().tz(userTz).utcOffset();
|
||||
|
||||
// midnight of user's time zone in utc time
|
||||
const startDateUTC = dayjs(travelSchedule.startDate).subtract(offset, "minute");
|
||||
// 23:59 of user's time zone in utc time
|
||||
const endDateUTC = dayjs(travelSchedule.endDate).subtract(offset, "minute");
|
||||
if (
|
||||
!dayjs.utc().isBefore(startDateUTC) &&
|
||||
dayjs.utc().isBefore(endDateUTC) &&
|
||||
!travelSchedule.prevTimeZone
|
||||
) {
|
||||
// if travel schedule has started and prevTimeZone is not yet set, we need to change time zone
|
||||
await setNewTimeZone(travelSchedule.timeZone, travelSchedule.user);
|
||||
|
||||
if (!travelSchedule.endDate) {
|
||||
travelScheduleIdsToDelete.push(travelSchedule.id);
|
||||
} else {
|
||||
await prisma.travelSchedule.update({
|
||||
where: {
|
||||
id: travelSchedule.id,
|
||||
},
|
||||
data: {
|
||||
prevTimeZone: travelSchedule.user.timeZone,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!dayjs.utc().isBefore(endDateUTC)) {
|
||||
if (travelSchedule.prevTimeZone) {
|
||||
// travel schedule ended, change back to original timezone
|
||||
await setNewTimeZone(travelSchedule.prevTimeZone, travelSchedule.user);
|
||||
}
|
||||
travelScheduleIdsToDelete.push(travelSchedule.id);
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.travelSchedule.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: travelScheduleIdsToDelete,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json({ timeZonesChanged });
|
||||
}
|
||||
@@ -286,7 +286,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
in: userIds as number[],
|
||||
},
|
||||
},
|
||||
select: { id: true, name: true, email: true, avatar: true, username: true },
|
||||
select: { id: true, name: true, email: true, avatarUrl: true, username: true },
|
||||
});
|
||||
|
||||
const userHashMap = new Map();
|
||||
|
||||
@@ -12,6 +12,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
||||
res.setHeader("Content-Type", "text/html");
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||
|
||||
res.write(
|
||||
await renderEmail("MonthlyDigestEmail", {
|
||||
language: t,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
||||
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
|
||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
@@ -15,7 +15,7 @@ import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["recorded-daily-video"] });
|
||||
const log = logger.getSubLogger({ prefix: ["daily-video-webhook-handler"] });
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
@@ -116,7 +116,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const response = schema.safeParse(req.body);
|
||||
|
||||
log.debug(
|
||||
"Recording Request Body:",
|
||||
"Daily video recording webhook Request Body:",
|
||||
safeStringify({
|
||||
response,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { createHmac } from "crypto";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient";
|
||||
import { sendDailyVideoTranscriptEmails } from "@calcom/emails";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
const testRequestSchema = z.object({
|
||||
test: z.enum(["test"]),
|
||||
});
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["send-daily-video-transcript-handler"] });
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
version: z.string(),
|
||||
type: z.string(),
|
||||
id: z.string(),
|
||||
payload: z
|
||||
.object({
|
||||
meeting_id: z.string(),
|
||||
end_ts: z.number().optional(),
|
||||
room: z.string(),
|
||||
start_ts: z.number().optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
event_ts: z.number().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (testRequestSchema.safeParse(req.body).success) {
|
||||
return res.status(200).json({ message: "Test request successful" });
|
||||
}
|
||||
|
||||
const hmacSecret = process.env.DAILY_MEETING_ENDED_WEBHOOK_SECRET;
|
||||
if (!hmacSecret) {
|
||||
return res.status(405).json({ message: "No Daily Webhook Secret" });
|
||||
}
|
||||
|
||||
const signature = `${req.headers["x-webhook-timestamp"]}.${JSON.stringify(req.body)}`;
|
||||
const base64DecodedSecret = Buffer.from(hmacSecret, "base64");
|
||||
const hmac = createHmac("sha256", base64DecodedSecret);
|
||||
const computed_signature = hmac.update(signature).digest("base64");
|
||||
|
||||
if (req.headers["x-webhook-signature"] !== computed_signature) {
|
||||
return res.status(403).json({ message: "Signature does not match" });
|
||||
}
|
||||
|
||||
const response = schema.safeParse(req.body);
|
||||
|
||||
log.debug(
|
||||
"Daily video transcript webhook Request Body:",
|
||||
safeStringify({
|
||||
response,
|
||||
})
|
||||
);
|
||||
|
||||
if (!response.success || response.data.type !== "meeting.ended") {
|
||||
return res.status(400).send({
|
||||
message: "Invalid Payload",
|
||||
});
|
||||
}
|
||||
|
||||
const { room, meeting_id } = response.data.payload;
|
||||
|
||||
try {
|
||||
const bookingReference = await prisma.bookingReference.findFirst({
|
||||
where: { type: "daily_video", uid: room, meetingId: room },
|
||||
select: { bookingId: true },
|
||||
});
|
||||
|
||||
if (!bookingReference || !bookingReference.bookingId) {
|
||||
log.error(
|
||||
"bookingReference Not found:",
|
||||
safeStringify({
|
||||
bookingReference,
|
||||
requestBody: req.body,
|
||||
})
|
||||
);
|
||||
return res.status(404).send({ message: "Booking reference not found" });
|
||||
}
|
||||
|
||||
const booking = await prisma.booking.findUniqueOrThrow({
|
||||
where: {
|
||||
id: bookingReference.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(
|
||||
"Booking Not Found:",
|
||||
safeStringify({
|
||||
booking,
|
||||
requestBody: req.body,
|
||||
})
|
||||
);
|
||||
|
||||
return res.status(404).send({
|
||||
message: `Booking of room_name ${room} does not exist or does not contain daily video as location`,
|
||||
});
|
||||
}
|
||||
|
||||
const transcripts = await getAllTranscriptsAccessLinkFromRoomName(room);
|
||||
|
||||
if (!transcripts || !transcripts.length)
|
||||
return res.status(200).json({ message: `No Transcripts found for room name ${room}` });
|
||||
|
||||
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);
|
||||
|
||||
// Send emails
|
||||
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,
|
||||
};
|
||||
|
||||
await sendDailyVideoTranscriptEmails(evt, transcripts);
|
||||
|
||||
return res.status(200).json({ message: "Success" });
|
||||
} catch (err) {
|
||||
console.error("Error in /send-daily-video-transcript", err);
|
||||
return res.status(500).json({ message: "something went wrong" });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: handler }),
|
||||
});
|
||||
@@ -1,154 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
orgDomainConfig,
|
||||
whereClauseForOrgWithSlugOrRequestedSlug,
|
||||
} from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["team/[slug]"] });
|
||||
const querySchema = z
|
||||
.object({
|
||||
username: z.string(),
|
||||
teamname: z.string(),
|
||||
/**
|
||||
* Passed when we want to fetch avatar of a particular organization
|
||||
*/
|
||||
orgSlug: z.string(),
|
||||
/**
|
||||
* Allow fetching avatar of a particular organization
|
||||
* Avatars being public, we need not worry about others accessing it.
|
||||
*/
|
||||
orgId: z.string().transform((s) => Number(s)),
|
||||
})
|
||||
.partial();
|
||||
|
||||
async function getIdentityData(req: NextApiRequest) {
|
||||
const { username, teamname, orgId, orgSlug } = querySchema.parse(req.query);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req);
|
||||
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
|
||||
const orgQuery = orgId
|
||||
? {
|
||||
id: orgId,
|
||||
}
|
||||
: org
|
||||
? whereClauseForOrgWithSlugOrRequestedSlug(org)
|
||||
: null;
|
||||
|
||||
if (username) {
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
...(orgQuery
|
||||
? {
|
||||
profiles: {
|
||||
some: {
|
||||
username,
|
||||
organization: orgQuery,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
username,
|
||||
// If a user is moved, it isn't actually available outside of the organization. So, for non-org domain check for movedToProfileId
|
||||
movedToProfileId: null,
|
||||
}),
|
||||
},
|
||||
select: { avatar: true, email: true },
|
||||
});
|
||||
|
||||
if (users.length > 1) {
|
||||
throw new Error(`More than one user found for username "${username}"`);
|
||||
}
|
||||
const [user] = users;
|
||||
return {
|
||||
name: username,
|
||||
email: user?.email,
|
||||
avatar: user?.avatar,
|
||||
org,
|
||||
};
|
||||
}
|
||||
|
||||
if (teamname) {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: teamname,
|
||||
parent: orgQuery,
|
||||
},
|
||||
select: { logo: true },
|
||||
});
|
||||
|
||||
return {
|
||||
org,
|
||||
name: teamname,
|
||||
email: null,
|
||||
avatar: getPlaceholderAvatar(team?.logo, teamname),
|
||||
};
|
||||
}
|
||||
|
||||
if (orgSlug) {
|
||||
const orgs = await prisma.team.findMany({
|
||||
where: {
|
||||
...whereClauseForOrgWithSlugOrRequestedSlug(orgSlug),
|
||||
},
|
||||
select: {
|
||||
slug: true,
|
||||
logo: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (orgs.length > 1) {
|
||||
// This should never happen, but instead of throwing error, we are just logging to be able to observe when it happens.
|
||||
log.error("More than one organization found for slug", orgSlug);
|
||||
}
|
||||
|
||||
const org = orgs[0];
|
||||
return {
|
||||
org: org?.slug,
|
||||
name: org?.name,
|
||||
email: null,
|
||||
avatar: getPlaceholderAvatar(org?.logo, org?.name),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const identity = await getIdentityData(req);
|
||||
const img = identity?.avatar;
|
||||
// If image isn't set or links to this route itself, use default avatar
|
||||
if (!img) {
|
||||
if (identity?.org) {
|
||||
res.setHeader("x-cal-org", identity.org);
|
||||
}
|
||||
res.writeHead(302, {
|
||||
Location: AVATAR_FALLBACK,
|
||||
});
|
||||
|
||||
return res.end();
|
||||
}
|
||||
|
||||
if (!img.includes("data:image")) {
|
||||
if (identity.org) {
|
||||
res.setHeader("x-cal-org", identity.org);
|
||||
}
|
||||
res.writeHead(302, { Location: img });
|
||||
return res.end();
|
||||
}
|
||||
|
||||
const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", "");
|
||||
const imageResp = Buffer.from(decoded, "base64");
|
||||
if (identity.org) {
|
||||
res.setHeader("x-cal-org", identity.org);
|
||||
}
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "image/png",
|
||||
"Content-Length": imageResp.length,
|
||||
});
|
||||
res.end(imageResp);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { GetStaticPropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import z from "zod";
|
||||
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Icon } from "@calcom/ui";
|
||||
|
||||
@@ -17,11 +17,9 @@ const querySchema = z.object({
|
||||
|
||||
export default function Error() {
|
||||
const { t } = useLocale();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { error } = querySchema.parse(searchParams);
|
||||
const isTokenVerificationError = error?.toLowerCase() === "verification";
|
||||
const errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const { error } = querySchema.parse({ error: searchParams?.get("error") || undefined });
|
||||
const errorMsg = error || t("error_during_login");
|
||||
return (
|
||||
<AuthContainer title="" description="">
|
||||
<div>
|
||||
@@ -30,11 +28,8 @@ export default function Error() {
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
{error}
|
||||
{errorMsg}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-subtle text-sm">{errorMsg}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-6">
|
||||
|
||||
@@ -25,15 +25,15 @@ function VerifyEmailChange(props: PageProps) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
async function updateSession() {
|
||||
async function updateSessionAndDisplayToast() {
|
||||
await update({ email: props.updatedEmail });
|
||||
router.push("/event-types");
|
||||
}
|
||||
if (props.updateSession) {
|
||||
updateSession();
|
||||
if (isLocaleReady) {
|
||||
showToast(t("verify_email_change_success_toast", { email: props.updatedEmail }), "success");
|
||||
}
|
||||
router.push("/event-types");
|
||||
}
|
||||
if (props.updateSession) {
|
||||
updateSessionAndDisplayToast();
|
||||
} else {
|
||||
if (isLocaleReady) {
|
||||
showToast(t("verify_email_change_failure_toast"), "error");
|
||||
|
||||
@@ -24,7 +24,7 @@ import SkeletonLoader from "@components/availability/SkeletonLoader";
|
||||
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
|
||||
const { t } = useLocale();
|
||||
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const meQuery = trpc.viewer.me.useQuery();
|
||||
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
import { Troubleshooter } from "@calcom/features/troubleshooter/Troubleshooter";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getLayout } from "@calcom/features/troubleshooter/layout";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
import { HeadSeo, Loader } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const TroubleshooterClientOnly = dynamic(
|
||||
() => import("@calcom/features/troubleshooter/Troubleshooter").then((mod) => mod.Troubleshooter),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
function TroubleshooterPage() {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<HeadSeo title={t("troubleshoot")} description={t("troubleshoot_availability")} />
|
||||
<Troubleshooter month={null} />
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Loader />
|
||||
</div>
|
||||
}>
|
||||
<TroubleshooterClientOnly month={null} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ export default function Type({
|
||||
isEmbed,
|
||||
user,
|
||||
booking,
|
||||
away,
|
||||
isBrandingHidden,
|
||||
isTeamEvent,
|
||||
entity,
|
||||
@@ -33,7 +32,6 @@ export default function Type({
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
bookingData={booking}
|
||||
isAway={away}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent={isTeamEvent}
|
||||
entity={entity}
|
||||
|
||||
@@ -3,31 +3,23 @@
|
||||
import Head from "next/head";
|
||||
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export default function MaintenancePage() {
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
if (!isLocaleReady) return null;
|
||||
return (
|
||||
<div className="bg-subtle flex h-screen">
|
||||
<Head>
|
||||
<title>
|
||||
{t("under_maintenance")} | {APP_NAME}
|
||||
</title>
|
||||
<title>Under Maintenance | {APP_NAME}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
||||
<h1 className="text-emphasis text-2xl font-medium">{t("under_maintenance")}</h1>
|
||||
<h1 className="text-emphasis text-2xl font-medium">Down for maintenance</h1>
|
||||
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">
|
||||
{t("under_maintenance_description", { appName: APP_NAME })}
|
||||
The Cal.com team are performing scheduled maintenance. If you have any questions, please contact
|
||||
support.
|
||||
</p>
|
||||
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
|
||||
<Button href={`${WEBSITE_URL}/support`}>Contact Support</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MaintenancePage.PageWrapper = PageWrapper;
|
||||
|
||||
@@ -21,7 +21,6 @@ export default function Type({
|
||||
slug,
|
||||
user,
|
||||
booking,
|
||||
away,
|
||||
isEmbed,
|
||||
isBrandingHidden,
|
||||
entity,
|
||||
@@ -42,7 +41,6 @@ export default function Type({
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
bookingData={booking}
|
||||
isAway={away}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
isInstantMeeting
|
||||
@@ -110,7 +108,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
eventData.length
|
||||
),
|
||||
booking: null,
|
||||
away: false,
|
||||
user: teamSlug,
|
||||
teamId: team.id,
|
||||
slug: meetingSlug,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user