Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6360e73901 | ||
|
|
7e44e686e8 | ||
|
|
0b914bef79 | ||
|
|
a48d190af3 | ||
|
|
c21f0caecf | ||
|
|
1f42a152b0 | ||
|
|
b8f0cc6fa1 | ||
|
|
c6f84e6c4b | ||
|
|
9c3f4ec055 | ||
|
|
f0cfbede7e | ||
|
|
e2a825a9a0 | ||
|
|
963a2a0b76 | ||
|
|
4037367f8c | ||
|
|
0b3b268eed | ||
|
|
2acb5559dc | ||
|
|
be1c6eb65c | ||
|
|
feefa26c90 | ||
|
|
2c25f88100 | ||
|
|
2be9fc8bc0 | ||
|
|
5927b42560 | ||
|
|
5726d18027 | ||
|
|
3312d9cfc6 | ||
|
|
c140b01548 | ||
|
|
c2085c6be9 | ||
|
|
b8b9d04897 | ||
|
|
4497c4ce0b | ||
|
|
cd06141816 | ||
|
|
013646bf2c | ||
|
|
12bbbe3617 | ||
|
|
2c7c9fd716 | ||
|
|
11fc2a738a | ||
|
|
44ab25b4a0 | ||
|
|
4a2c0cb164 | ||
|
|
b1d9e2b84c | ||
|
|
47b9e8a8ac | ||
|
|
42265021fe | ||
|
|
0b0fee3985 | ||
|
|
5718cc5e17 | ||
|
|
361889ded7 | ||
|
|
2260ddcebc | ||
|
|
80c3b91077 | ||
|
|
a11cf130bf | ||
|
|
f9d6ea2a00 | ||
|
|
50f6aa5f58 | ||
|
|
132746af74 | ||
|
|
395381ddcc | ||
|
|
ef88effd9f | ||
|
|
673a2be7c7 | ||
|
|
b1cd3e940e | ||
|
|
711bec8d71 | ||
|
|
7529e331a3 | ||
|
|
cc147d0797 | ||
|
|
c8e8800647 | ||
|
|
99caae8665 | ||
|
|
1f08bc5a4f | ||
|
|
5594e5c713 | ||
|
|
34afc7b674 | ||
|
|
513a58c91c | ||
|
|
375b9190a3 | ||
|
|
83d7bf322a | ||
|
|
06429e27fb | ||
|
|
90bcd711a9 | ||
|
|
8d61b452b1 | ||
|
|
03d0b98155 | ||
|
|
de503add83 | ||
|
|
1a4cf5f91d | ||
|
|
ed53645783 | ||
|
|
c7b1fbdf67 | ||
|
|
6d7f7108e5 | ||
|
|
d9aab9088d | ||
|
|
c80daf43b8 | ||
|
|
2bdf65ce8c | ||
|
|
343e2ac9d7 | ||
|
|
670b82fd23 | ||
|
|
e6f81d09c8 | ||
|
|
8f91ffe692 | ||
|
|
f5dbdae4a9 | ||
|
|
e99f6048d6 | ||
|
|
654a204878 | ||
|
|
7928933852 | ||
|
|
c4cdba1015 | ||
|
|
5ed36a7fe1 | ||
|
|
e77a2f6963 | ||
|
|
a10d3599e2 | ||
|
|
d99f171597 | ||
|
|
da55306a5a | ||
|
|
66378160df | ||
|
|
8aee8d9d3b | ||
|
|
79e33a4d26 | ||
|
|
b5fa43f086 | ||
|
|
0d757039f7 | ||
|
|
44f6f2e320 | ||
|
|
d8eb1df61d | ||
|
|
ef7cac6c50 | ||
|
|
21dd44733f | ||
|
|
b0bfec7247 | ||
|
|
ba5b9e951a | ||
|
|
5e3865b645 | ||
|
|
a02240b579 | ||
|
|
376caa412d | ||
|
|
8722745185 | ||
|
+12 |
a4a3eb5ea9 | ||
|
|
b560475d89 | ||
|
|
4872daef60 | ||
|
|
dfa92f5cb6 | ||
|
|
babb1a3bf7 | ||
|
|
7b5a2726ab | ||
|
|
7d4653296e | ||
|
|
644fb38f87 | ||
|
|
743b155a88 | ||
|
|
664a92a1d0 | ||
|
|
8ae71f6160 | ||
|
|
ab1eab9c54 | ||
|
|
a64b7752c9 | ||
|
|
be728e9c01 | ||
|
|
2146c04444 | ||
|
|
0b94473c7f | ||
|
|
2a1dc619ee | ||
|
|
48a2fc2f67 | ||
|
|
4553455098 | ||
|
|
b4f1b5aebe | ||
|
|
a99e285d76 | ||
|
|
bdea436f8b | ||
|
|
182d0fa228 | ||
|
|
0d7f560c43 |
+1
-1
@@ -168,13 +168,13 @@ NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
|
||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||
STRIPE_TEAM_PRODUCT_ID=
|
||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET_APPS=
|
||||
STRIPE_PRIVATE_KEY=
|
||||
STRIPE_CLIENT_ID=
|
||||
|
||||
|
||||
# Use for internal Public API Keys and optional
|
||||
API_KEY_PREFIX=cal_
|
||||
# ***********************************************************************************************************
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
## Mandatory Tasks (DO NOT REMOVE)
|
||||
|
||||
- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected).
|
||||
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com/docs). If N/A, write N/A here and check the checkbox.
|
||||
- [ ] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox.
|
||||
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works.
|
||||
|
||||
## How should this be tested?
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# This is just to test this file
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docs
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- name: Cache Docs build
|
||||
uses: buildjet/cache@v4
|
||||
id: cache-docs-build
|
||||
env:
|
||||
cache-name: docs-build
|
||||
key-1: ${{ hashFiles('yarn.lock') }}
|
||||
key-2: ${{ hashFiles('docs/**.*', '!**/node_modules') }}
|
||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||
key-4: ${{ github.sha }}
|
||||
with:
|
||||
path: |
|
||||
**/docs/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- name: Run build
|
||||
working-directory: docs
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
if [ ${{ steps.cache-docs-build.outputs.cache-hit }} == 'true' ]; then
|
||||
echo "Cache hit for Docs build. Skipping build."
|
||||
else
|
||||
npm install -g mintlify
|
||||
mintlify dev &
|
||||
sleep 5 # Let it run for 5 seconds
|
||||
kill $!
|
||||
fi
|
||||
shell: bash
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
with:
|
||||
filters: |
|
||||
has-files-requiring-all-checks:
|
||||
- "!(**.md|.github/CODEOWNERS)"
|
||||
- "!(**.md|.github/CODEOWNERS|docs/**|apps/web/public/static/locales/**/common.json)"
|
||||
- name: Get Latest Commit SHA
|
||||
id: get_sha
|
||||
run: |
|
||||
@@ -139,6 +139,13 @@ jobs:
|
||||
uses: ./.github/workflows/atoms-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-docs:
|
||||
name: Production builds
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/docs-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production builds
|
||||
needs: [changes, check-label, deps]
|
||||
@@ -225,6 +232,7 @@ jobs:
|
||||
build-api-v1,
|
||||
build-api-v2,
|
||||
build-atoms,
|
||||
build-docs,
|
||||
e2e,
|
||||
e2e-api-v2,
|
||||
e2e-embed,
|
||||
|
||||
@@ -39,7 +39,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Web App
|
||||
name: Build Web App
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { emailSchema } from "@calcom/lib/emailSchema";
|
||||
import { _AttendeeModel as Attendee } from "@calcom/prisma/zod";
|
||||
|
||||
import { timeZone } from "~/lib/validations/shared/timeZone";
|
||||
@@ -14,7 +15,7 @@ export const schemaAttendeeBaseBodyParams = Attendee.pick({
|
||||
const schemaAttendeeCreateParams = z
|
||||
.object({
|
||||
bookingId: z.number().int(),
|
||||
email: z.string().email(),
|
||||
email: emailSchema,
|
||||
name: z.string(),
|
||||
timeZone: timeZone,
|
||||
})
|
||||
@@ -23,7 +24,7 @@ const schemaAttendeeCreateParams = z
|
||||
const schemaAttendeeEditParams = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
email: emailSchema.optional(),
|
||||
timeZone: timeZone.optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -73,6 +73,7 @@ export const schemaBookingReadPublic = Booking.extend({
|
||||
attendees: z
|
||||
.array(
|
||||
_AttendeeModel.pick({
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
timeZone: true,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { withValidation } from "next-validations";
|
||||
import { z } from "zod";
|
||||
|
||||
import { emailSchema } from "@calcom/lib/emailSchema";
|
||||
|
||||
import { baseApiParams } from "./baseApiParams";
|
||||
|
||||
// Extracted out as utility function so can be reused
|
||||
// at different endpoints that require this validation.
|
||||
export const schemaQueryAttendeeEmail = baseApiParams.extend({
|
||||
attendeeEmail: z.string().email(),
|
||||
attendeeEmail: emailSchema,
|
||||
});
|
||||
|
||||
export const schemaQuerySingleOrMultipleAttendeeEmails = z.object({
|
||||
attendeeEmail: z.union([z.string().email(), z.array(z.string().email())]).optional(),
|
||||
attendeeEmail: z.union([emailSchema, z.array(emailSchema)]).optional(),
|
||||
});
|
||||
|
||||
export const withValidQueryAttendeeEmail = withValidation({
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { withValidation } from "next-validations";
|
||||
import { z } from "zod";
|
||||
|
||||
import { emailSchema } from "@calcom/lib/emailSchema";
|
||||
|
||||
import { baseApiParams } from "./baseApiParams";
|
||||
|
||||
// Extracted out as utility function so can be reused
|
||||
// at different endpoints that require this validation.
|
||||
export const schemaQueryUserEmail = baseApiParams.extend({
|
||||
email: z.string().email(),
|
||||
email: emailSchema,
|
||||
});
|
||||
|
||||
export const schemaQuerySingleOrMultipleUserEmails = z.object({
|
||||
email: z.union([z.string().email(), z.array(z.string().email())]),
|
||||
email: z.union([emailSchema, z.array(emailSchema)]),
|
||||
});
|
||||
|
||||
export const withValidQueryUserEmail = withValidation({
|
||||
|
||||
@@ -11,6 +11,7 @@ export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true })
|
||||
smsLockState: true,
|
||||
smsLockReviewedByAdmin: true,
|
||||
bookingLimits: true,
|
||||
includeManagedEventsInLimits: true,
|
||||
});
|
||||
|
||||
const schemaTeamRequiredParams = z.object({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { emailSchema } from "@calcom/lib/emailSchema";
|
||||
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
||||
import { _UserModel as User } from "@calcom/prisma/zod";
|
||||
import { iso8601 } from "@calcom/prisma/zod-utils";
|
||||
@@ -92,7 +93,7 @@ export const schemaUserBaseBodyParams = User.pick({
|
||||
// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value
|
||||
// for example making weekStart only accept weekdays as input
|
||||
const schemaUserEditParams = z.object({
|
||||
email: z.string().email().toLowerCase(),
|
||||
email: emailSchema.toLowerCase(),
|
||||
username: usernameSchema,
|
||||
weekStart: z.nativeEnum(weekdays).optional(),
|
||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||
@@ -115,7 +116,7 @@ const schemaUserEditParams = z.object({
|
||||
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
|
||||
|
||||
const schemaUserCreateParams = z.object({
|
||||
email: z.string().email().toLowerCase(),
|
||||
email: emailSchema.toLowerCase(),
|
||||
username: usernameSchema,
|
||||
weekStart: z.nativeEnum(weekdays).optional(),
|
||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@sentry/nextjs": "^8.8.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"memory-cache": "^0.2.0",
|
||||
"next": "^13.5.4",
|
||||
"next": "^13.5.6",
|
||||
"next-api-middleware": "^1.0.1",
|
||||
"next-axiom": "^0.17.0",
|
||||
"next-swagger-doc": "^0.3.6",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"dependencies": {
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.44",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.45",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -3,8 +3,6 @@ import { Controller, Get, Version, VERSION_NEUTRAL } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||
|
||||
@Controller()
|
||||
@DocsTags("Health - development only")
|
||||
@DocsExcludeController(getEnv("NODE_ENV") === "production")
|
||||
export class AppController {
|
||||
@Get("health")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
|
||||
@@ -14,7 +14,7 @@ export class BookingsRepository_2024_08_13 {
|
||||
});
|
||||
}
|
||||
|
||||
async getByIdsWithAttendeesAndUser(ids: number[]) {
|
||||
async getByIdsWithAttendeesAndUserAndEvent(ids: number[]) {
|
||||
return this.dbRead.prisma.booking.findMany({
|
||||
where: {
|
||||
id: {
|
||||
@@ -24,6 +24,7 @@ export class BookingsRepository_2024_08_13 {
|
||||
include: {
|
||||
attendees: true,
|
||||
user: true,
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -36,7 +37,7 @@ export class BookingsRepository_2024_08_13 {
|
||||
});
|
||||
}
|
||||
|
||||
async getByIdWithAttendeesAndUser(id: number) {
|
||||
async getByIdWithAttendeesAndUserAndEvent(id: number) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
id,
|
||||
@@ -44,11 +45,12 @@ export class BookingsRepository_2024_08_13 {
|
||||
include: {
|
||||
attendees: true,
|
||||
user: true,
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByUidWithAttendeesAndUser(uid: string) {
|
||||
async getByUidWithAttendeesAndUserAndEvent(uid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
uid,
|
||||
@@ -56,11 +58,12 @@ export class BookingsRepository_2024_08_13 {
|
||||
include: {
|
||||
attendees: true,
|
||||
user: true,
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getRecurringByUidWithAttendeesAndUser(uid: string) {
|
||||
async getRecurringByUidWithAttendeesAndUserAndEvent(uid: string) {
|
||||
return this.dbRead.prisma.booking.findMany({
|
||||
where: {
|
||||
recurringEventId: uid,
|
||||
@@ -68,6 +71,7 @@ export class BookingsRepository_2024_08_13 {
|
||||
include: {
|
||||
attendees: true,
|
||||
user: true,
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
||||
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output";
|
||||
import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output";
|
||||
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
@@ -36,6 +34,7 @@ import { User } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
|
||||
import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetBookingOutput_2024_08_13, GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInputPipe,
|
||||
CreateBookingInput,
|
||||
|
||||
+1
-1
@@ -1,7 +1,6 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
|
||||
+327
-25
@@ -2,8 +2,6 @@ import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output";
|
||||
import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output";
|
||||
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
@@ -30,6 +28,7 @@ import {
|
||||
VERSION_2024_08_13,
|
||||
X_CAL_CLIENT_ID,
|
||||
} from "@calcom/platform-constants";
|
||||
import { GetBookingOutput_2024_08_13, GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
@@ -58,6 +57,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
let user: User;
|
||||
|
||||
let eventTypeId: number;
|
||||
const eventTypeSlug = "peer-coding";
|
||||
let recurringEventTypeId: number;
|
||||
|
||||
let createdBooking: BookingOutput_2024_08_13;
|
||||
@@ -100,7 +100,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
const event = await eventTypesRepositoryFixture.create(
|
||||
{ title: "peer coding", slug: "peer-coding", length: 60 },
|
||||
{ title: "peer coding", slug: eventTypeSlug, length: 60 },
|
||||
user.id
|
||||
);
|
||||
eventTypeId = event.id;
|
||||
@@ -184,7 +184,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -208,14 +211,24 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString());
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(eventTypeId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: eventTypeId,
|
||||
slug: eventTypeSlug,
|
||||
});
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(data.location).toEqual(body.location);
|
||||
expect(data.meetingUrl).toEqual(body.location);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
expect(data.bookingFieldsResponses).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
...body.bookingFieldsResponses,
|
||||
});
|
||||
createdBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
@@ -235,7 +248,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -268,7 +281,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(firstBooking.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(firstBooking.location).toEqual(body.location);
|
||||
expect(firstBooking.recurringBookingUid).toBeDefined();
|
||||
expect(firstBooking.absentHost).toEqual(false);
|
||||
|
||||
@@ -288,7 +301,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(secondBooking.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(secondBooking.location).toEqual(body.location);
|
||||
expect(secondBooking.absentHost).toEqual(false);
|
||||
|
||||
const thirdBooking = data[2];
|
||||
@@ -307,7 +320,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(thirdBooking.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(thirdBooking.location).toEqual(body.location);
|
||||
expect(thirdBooking.absentHost).toEqual(false);
|
||||
|
||||
createdRecurringBooking = data;
|
||||
@@ -343,7 +356,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.duration).toEqual(createdBooking.duration);
|
||||
expect(data.eventTypeId).toEqual(createdBooking.eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
|
||||
expect(data.meetingUrl).toEqual(createdBooking.meetingUrl);
|
||||
expect(data.location).toEqual(createdBooking.location);
|
||||
expect(data.absentHost).toEqual(createdBooking.absentHost);
|
||||
} else {
|
||||
throw new Error(
|
||||
@@ -377,7 +390,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
|
||||
expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid);
|
||||
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
|
||||
expect(data.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl);
|
||||
expect(data.location).toEqual(createdRecurringBooking[0].location);
|
||||
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
|
||||
} else {
|
||||
throw new Error(
|
||||
@@ -414,7 +427,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(firstBooking.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
|
||||
expect(firstBooking.recurringBookingUid).toEqual(recurringBookingUid);
|
||||
expect(firstBooking.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
|
||||
expect(firstBooking.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl);
|
||||
expect(firstBooking.location).toEqual(createdRecurringBooking[0].location);
|
||||
expect(firstBooking.absentHost).toEqual(createdRecurringBooking[0].absentHost);
|
||||
|
||||
const secondBooking = data[1];
|
||||
@@ -428,7 +441,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(secondBooking.eventTypeId).toEqual(createdRecurringBooking[1].eventTypeId);
|
||||
expect(secondBooking.recurringBookingUid).toEqual(recurringBookingUid);
|
||||
expect(secondBooking.attendees[0]).toEqual(createdRecurringBooking[1].attendees[0]);
|
||||
expect(secondBooking.meetingUrl).toEqual(createdRecurringBooking[1].meetingUrl);
|
||||
expect(secondBooking.location).toEqual(createdRecurringBooking[1].location);
|
||||
expect(secondBooking.absentHost).toEqual(createdRecurringBooking[1].absentHost);
|
||||
|
||||
const thirdBooking = data[2];
|
||||
@@ -442,7 +455,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(thirdBooking.eventTypeId).toEqual(createdRecurringBooking[2].eventTypeId);
|
||||
expect(thirdBooking.recurringBookingUid).toEqual(recurringBookingUid);
|
||||
expect(thirdBooking.attendees[0]).toEqual(createdRecurringBooking[2].attendees[0]);
|
||||
expect(thirdBooking.meetingUrl).toEqual(createdRecurringBooking[2].meetingUrl);
|
||||
expect(thirdBooking.location).toEqual(createdRecurringBooking[2].location);
|
||||
expect(thirdBooking.absentHost).toEqual(createdRecurringBooking[2].absentHost);
|
||||
|
||||
createdRecurringBooking = data;
|
||||
@@ -764,7 +777,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.duration).toEqual(createdBooking.duration);
|
||||
expect(data.eventTypeId).toEqual(createdBooking.eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
|
||||
expect(data.meetingUrl).toEqual(createdBooking.meetingUrl);
|
||||
expect(data.location).toEqual(createdBooking.location);
|
||||
expect(data.absentHost).toEqual(createdBooking.absentHost);
|
||||
|
||||
rescheduledBooking = data;
|
||||
@@ -815,7 +828,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid);
|
||||
expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
|
||||
expect(data.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl);
|
||||
expect(data.location).toEqual(createdRecurringBooking[0].location);
|
||||
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
|
||||
|
||||
const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid);
|
||||
@@ -885,7 +898,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.duration).toEqual(booking.duration);
|
||||
expect(data.eventTypeId).toEqual(booking.eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(booking.attendees[0]);
|
||||
expect(data.meetingUrl).toEqual(booking.meetingUrl);
|
||||
expect(data.location).toEqual(booking.location);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -918,7 +931,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.eventTypeId).toEqual(booking.eventTypeId);
|
||||
expect(data.attendees[0].absent).toEqual(true);
|
||||
expect(data.absentHost).toEqual(booking.absentHost);
|
||||
expect(data.meetingUrl).toEqual(booking.meetingUrl);
|
||||
expect(data.location).toEqual(booking.location);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -955,7 +968,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.duration).toEqual(rescheduledBooking.duration);
|
||||
expect(data.eventTypeId).toEqual(rescheduledBooking.eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(rescheduledBooking.attendees[0]);
|
||||
expect(data.meetingUrl).toEqual(rescheduledBooking.meetingUrl);
|
||||
expect(data.location).toEqual(rescheduledBooking.location);
|
||||
expect(data.absentHost).toEqual(rescheduledBooking.absentHost);
|
||||
|
||||
const cancelledBooking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid);
|
||||
@@ -1005,10 +1018,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
|
||||
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
|
||||
return (
|
||||
!Array.isArray(data) &&
|
||||
@@ -1019,10 +1028,303 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
);
|
||||
}
|
||||
|
||||
function responseDataIsRecurringBooking(data: any): data is RecurringBookingOutput_2024_08_13[] {
|
||||
return Array.isArray(data);
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
|
||||
function responseDataIsRecurringBooking(data: any): data is RecurringBookingOutput_2024_08_13[] {
|
||||
return Array.isArray(data);
|
||||
}
|
||||
|
||||
describe("Recurring bookings", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
|
||||
const userEmail = "bookings-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
const maxRecurrenceCount = 3;
|
||||
let recurringEventTypeId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
|
||||
const recurringEvent = await eventTypesRepositoryFixture.create(
|
||||
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
|
||||
{
|
||||
title: "peer coding recurring",
|
||||
slug: "peer-coding-recurring",
|
||||
length: 60,
|
||||
recurringEvent: { freq: 2, count: maxRecurrenceCount, interval: 1 },
|
||||
},
|
||||
user.id
|
||||
);
|
||||
recurringEventTypeId = recurringEvent.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("should not create recurring booking with recurrenceCount larger than event type recurrence count", async () => {
|
||||
const recurrenceCount = 1000;
|
||||
|
||||
const body: CreateRecurringBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: recurringEventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper Recurring",
|
||||
email: "mr_proper_recurring@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
recurrenceCount,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should create a recurring booking with recurrenceCount smaller than event type recurrence count", async () => {
|
||||
const recurrenceCount = maxRecurrenceCount - 1;
|
||||
const body: CreateRecurringBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: recurringEventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper Recurring",
|
||||
email: "mr_proper_recurring@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
recurrenceCount,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsRecurringBooking(responseBody.data)) {
|
||||
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
|
||||
expect(data.length).toEqual(maxRecurrenceCount - 1);
|
||||
|
||||
const firstBooking = data[0];
|
||||
expect(firstBooking.id).toBeDefined();
|
||||
expect(firstBooking.uid).toBeDefined();
|
||||
expect(firstBooking.hosts[0].id).toEqual(user.id);
|
||||
expect(firstBooking.status).toEqual("accepted");
|
||||
expect(firstBooking.start).toEqual(new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString());
|
||||
expect(firstBooking.end).toEqual(new Date(Date.UTC(2030, 1, 4, 14, 0, 0)).toISOString());
|
||||
expect(firstBooking.duration).toEqual(60);
|
||||
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(firstBooking.location).toEqual(body.location);
|
||||
expect(firstBooking.recurringBookingUid).toBeDefined();
|
||||
expect(firstBooking.absentHost).toEqual(false);
|
||||
|
||||
const secondBooking = data[1];
|
||||
expect(secondBooking.id).toBeDefined();
|
||||
expect(secondBooking.uid).toBeDefined();
|
||||
expect(secondBooking.hosts[0].id).toEqual(user.id);
|
||||
expect(secondBooking.status).toEqual("accepted");
|
||||
expect(secondBooking.start).toEqual(new Date(Date.UTC(2030, 1, 11, 13, 0, 0)).toISOString());
|
||||
expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString());
|
||||
expect(secondBooking.duration).toEqual(60);
|
||||
expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId);
|
||||
expect(secondBooking.recurringBookingUid).toBeDefined();
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(secondBooking.location).toEqual(body.location);
|
||||
expect(secondBooking.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a recurring booking with recurrenceCount equal to event type recurrence count", async () => {
|
||||
const recurrenceCount = maxRecurrenceCount;
|
||||
const body: CreateRecurringBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: recurringEventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper Recurring",
|
||||
email: "mr_proper_recurring@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
recurrenceCount,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsRecurringBooking(responseBody.data)) {
|
||||
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
|
||||
expect(data.length).toEqual(maxRecurrenceCount);
|
||||
|
||||
const firstBooking = data[0];
|
||||
expect(firstBooking.id).toBeDefined();
|
||||
expect(firstBooking.uid).toBeDefined();
|
||||
expect(firstBooking.hosts[0].id).toEqual(user.id);
|
||||
expect(firstBooking.status).toEqual("accepted");
|
||||
expect(firstBooking.start).toEqual(new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString());
|
||||
expect(firstBooking.end).toEqual(new Date(Date.UTC(2030, 1, 4, 14, 0, 0)).toISOString());
|
||||
expect(firstBooking.duration).toEqual(60);
|
||||
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(firstBooking.location).toEqual(body.location);
|
||||
expect(firstBooking.meetingUrl).toEqual(body.location);
|
||||
expect(firstBooking.recurringBookingUid).toBeDefined();
|
||||
expect(firstBooking.absentHost).toEqual(false);
|
||||
|
||||
const secondBooking = data[1];
|
||||
expect(secondBooking.id).toBeDefined();
|
||||
expect(secondBooking.uid).toBeDefined();
|
||||
expect(secondBooking.hosts[0].id).toEqual(user.id);
|
||||
expect(secondBooking.status).toEqual("accepted");
|
||||
expect(secondBooking.start).toEqual(new Date(Date.UTC(2030, 1, 11, 13, 0, 0)).toISOString());
|
||||
expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString());
|
||||
expect(secondBooking.duration).toEqual(60);
|
||||
expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId);
|
||||
expect(secondBooking.recurringBookingUid).toBeDefined();
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(secondBooking.location).toEqual(body.location);
|
||||
expect(secondBooking.absentHost).toEqual(false);
|
||||
|
||||
const thirdBooking = data[2];
|
||||
expect(thirdBooking.id).toBeDefined();
|
||||
expect(thirdBooking.uid).toBeDefined();
|
||||
expect(thirdBooking.hosts[0].id).toEqual(user.id);
|
||||
expect(thirdBooking.status).toEqual("accepted");
|
||||
expect(thirdBooking.start).toEqual(new Date(Date.UTC(2030, 1, 18, 13, 0, 0)).toISOString());
|
||||
expect(thirdBooking.end).toEqual(new Date(Date.UTC(2030, 1, 18, 14, 0, 0)).toISOString());
|
||||
expect(thirdBooking.duration).toEqual(60);
|
||||
expect(thirdBooking.eventTypeId).toEqual(recurringEventTypeId);
|
||||
expect(thirdBooking.recurringBookingUid).toBeDefined();
|
||||
expect(thirdBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(thirdBooking.location).toEqual(body.location);
|
||||
expect(thirdBooking.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
@@ -73,7 +73,9 @@ export class BookingsService_2024_08_13 {
|
||||
const bookingRequest = await this.inputService.createBookingRequest(request, body);
|
||||
const booking = await handleInstantMeeting(bookingRequest);
|
||||
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.bookingId);
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(
|
||||
booking.bookingId
|
||||
);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${booking.bookingId} was not found in the database`);
|
||||
}
|
||||
@@ -94,7 +96,7 @@ export class BookingsService_2024_08_13 {
|
||||
throw new Error("Recurring booking was not created");
|
||||
}
|
||||
|
||||
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUser(uid);
|
||||
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUserAndEvent(uid);
|
||||
return this.outputService.getOutputRecurringBookings(recurringBooking);
|
||||
}
|
||||
|
||||
@@ -106,7 +108,7 @@ export class BookingsService_2024_08_13 {
|
||||
throw new Error("Booking was not created");
|
||||
}
|
||||
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.id);
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(booking.id);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${booking.id} was not found in the database`);
|
||||
}
|
||||
@@ -115,7 +117,7 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
async getBooking(uid: string) {
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUser(uid);
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(uid);
|
||||
|
||||
if (booking) {
|
||||
const isRecurring = !!booking.recurringEventId;
|
||||
@@ -125,7 +127,7 @@ export class BookingsService_2024_08_13 {
|
||||
return this.outputService.getOutputBooking(booking);
|
||||
}
|
||||
|
||||
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUser(uid);
|
||||
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUserAndEvent(uid);
|
||||
if (!recurringBooking.length) {
|
||||
throw new NotFoundException(`Booking with uid=${uid} was not found in the database`);
|
||||
}
|
||||
@@ -149,7 +151,7 @@ export class BookingsService_2024_08_13 {
|
||||
// note(Lauris): fetchedBookings don't have attendees information and responses and i don't want to add them to the handler query,
|
||||
// because its used elsewhere in code that does not need that information, so i get ids, fetch bookings and then return them formatted in same order as ids.
|
||||
const ids = fetchedBookings.bookings.map((booking) => booking.id);
|
||||
const bookings = await this.bookingsRepository.getByIdsWithAttendeesAndUser(ids);
|
||||
const bookings = await this.bookingsRepository.getByIdsWithAttendeesAndUserAndEvent(ids);
|
||||
|
||||
const bookingMap = new Map(bookings.map((booking) => [booking.id, booking]));
|
||||
const orderedBookings = ids.map((id) => bookingMap.get(id));
|
||||
@@ -162,6 +164,7 @@ export class BookingsService_2024_08_13 {
|
||||
|
||||
const formatted = {
|
||||
...booking,
|
||||
eventType: booking.eventType,
|
||||
eventTypeId: booking.eventTypeId,
|
||||
startTime: new Date(booking.startTime),
|
||||
endTime: new Date(booking.endTime),
|
||||
@@ -191,7 +194,7 @@ export class BookingsService_2024_08_13 {
|
||||
throw new Error("Booking was not created");
|
||||
}
|
||||
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.id);
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(booking.id);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${booking.id} was not found in the database`);
|
||||
}
|
||||
@@ -226,7 +229,7 @@ export class BookingsService_2024_08_13 {
|
||||
userId: bookingOwnerId,
|
||||
});
|
||||
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUser(bookingUid);
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
|
||||
|
||||
if (!booking) {
|
||||
throw new Error(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Request } from "express";
|
||||
@@ -57,9 +57,9 @@ export enum Frequency {
|
||||
|
||||
const recurringEventSchema = z.object({
|
||||
dtstart: z.string().optional(),
|
||||
interval: z.number().int().optional(),
|
||||
count: z.number().int().optional(),
|
||||
freq: z.nativeEnum(Frequency).optional(),
|
||||
interval: z.number().int(),
|
||||
count: z.number().int(),
|
||||
freq: z.nativeEnum(Frequency),
|
||||
until: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
|
||||
const location = request.body.meetingUrl;
|
||||
const location = request.body.location || request.body.meetingUrl;
|
||||
Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location });
|
||||
|
||||
newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled };
|
||||
@@ -147,7 +147,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
|
||||
const location = request.body.meetingUrl;
|
||||
const location = request.body.location || request.body.meetingUrl;
|
||||
Object.assign(newRequest, {
|
||||
userId,
|
||||
...oAuthParams,
|
||||
@@ -175,14 +175,16 @@ export class InputBookingsService_2024_08_13 {
|
||||
|
||||
const occurrence = recurringEventSchema.parse(eventType.recurringEvent);
|
||||
const repeatsEvery = occurrence.interval;
|
||||
const repeatsTimes = occurrence.count;
|
||||
|
||||
if (inputBooking.recurrenceCount && inputBooking.recurrenceCount > occurrence.count) {
|
||||
throw new BadRequestException(
|
||||
"Provided recurrence count is higher than the event type's recurring event count."
|
||||
);
|
||||
}
|
||||
const repeatsTimes = inputBooking.recurrenceCount || occurrence.count;
|
||||
// note(Lauris): timeBetween 0=yearly, 1=monthly and 2=weekly
|
||||
const timeBetween = occurrence.freq;
|
||||
|
||||
if (!repeatsTimes) {
|
||||
throw new Error("Repeats times is required");
|
||||
}
|
||||
|
||||
const events = [];
|
||||
const recurringEventId = uuidv4();
|
||||
|
||||
@@ -257,7 +259,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
async transformInputRescheduleBooking(bookingUid: string, inputBooking: RescheduleBookingInput_2024_08_13) {
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUser(bookingUid);
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
|
||||
}
|
||||
@@ -390,7 +392,9 @@ export class InputBookingsService_2024_08_13 {
|
||||
async transformInputCancelBooking(bookingUid: string, inputBooking: CancelBookingInput_2024_08_13) {
|
||||
let allRemainingBookings = false;
|
||||
let uid = bookingUid;
|
||||
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUser(bookingUid);
|
||||
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUserAndEvent(
|
||||
bookingUid
|
||||
);
|
||||
|
||||
if (recurringBooking.length) {
|
||||
// note(Lauirs): this means that bookingUid is equal to recurringEventId on individual bookings of recurring one aka main recurring event
|
||||
|
||||
@@ -7,14 +7,20 @@ import { z } from "zod";
|
||||
import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { Booking } from "@calcom/prisma/client";
|
||||
|
||||
export const bookingResponsesSchema = z.object({
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
guests: z.array(z.string()).optional(),
|
||||
rescheduledReason: z.string().optional(),
|
||||
});
|
||||
export const bookingResponsesSchema = z
|
||||
.object({
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
guests: z.array(z.string()).optional(),
|
||||
rescheduledReason: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
type DatabaseBooking = Booking & {
|
||||
eventType: {
|
||||
id: number;
|
||||
slug: string;
|
||||
} | null;
|
||||
attendees: {
|
||||
name: string;
|
||||
email: string;
|
||||
@@ -38,6 +44,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
uid: databaseBooking.uid,
|
||||
title: databaseBooking.title,
|
||||
description: databaseBooking.description,
|
||||
hosts: [databaseBooking.user],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
||||
@@ -46,6 +54,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
start: databaseBooking.startTime,
|
||||
end: databaseBooking.endTime,
|
||||
duration,
|
||||
eventType: databaseBooking.eventType,
|
||||
// note(Lauris): eventTypeId is deprecated
|
||||
eventTypeId: databaseBooking.eventTypeId,
|
||||
attendees: databaseBooking.attendees.map((attendee) => ({
|
||||
name: attendee.name,
|
||||
@@ -55,18 +65,22 @@ export class OutputBookingsService_2024_08_13 {
|
||||
absent: !!attendee.noShow,
|
||||
})),
|
||||
guests: bookingResponses.guests,
|
||||
location: databaseBooking.location,
|
||||
// note(Lauris): meetingUrl is deprecated
|
||||
meetingUrl: databaseBooking.location,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
};
|
||||
|
||||
return plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
bookingTransformed.bookingFieldsResponses = bookingResponses;
|
||||
return bookingTransformed;
|
||||
}
|
||||
|
||||
async getOutputRecurringBookings(databaseBookings: DatabaseBooking[]) {
|
||||
const transformed = [];
|
||||
|
||||
for (const booking of databaseBookings) {
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.id);
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(booking.id);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${booking.id} was not found in the database`);
|
||||
}
|
||||
@@ -87,6 +101,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
uid: databaseBooking.uid,
|
||||
title: databaseBooking.title,
|
||||
description: databaseBooking.description,
|
||||
hosts: [databaseBooking.user],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
||||
@@ -95,6 +111,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
start: databaseBooking.startTime,
|
||||
end: databaseBooking.endTime,
|
||||
duration,
|
||||
eventType: databaseBooking.eventType,
|
||||
// note(Lauris): eventTypeId is deprecated
|
||||
eventTypeId: databaseBooking.eventTypeId,
|
||||
attendees: databaseBooking.attendees.map((attendee) => ({
|
||||
name: attendee.name,
|
||||
@@ -104,9 +122,12 @@ export class OutputBookingsService_2024_08_13 {
|
||||
absent: !!attendee.noShow,
|
||||
})),
|
||||
guests: bookingResponses.guests,
|
||||
location: databaseBooking.location,
|
||||
// note(Lauris): meetingUrl is deprecated
|
||||
meetingUrl: databaseBooking.location,
|
||||
recurringBookingUid: databaseBooking.recurringEventId,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
bookingFieldsResponses: databaseBooking.responses,
|
||||
};
|
||||
|
||||
return plainToClass(RecurringBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
|
||||
@@ -21,6 +21,9 @@ export type Environment = {
|
||||
API_KEY_PREFIX: string;
|
||||
DOCS_URL: string;
|
||||
RATE_LIMIT_DEFAULT_TTL_MS: number;
|
||||
RATE_LIMIT_DEFAULT_LIMIT_API_KEY: number;
|
||||
RATE_LIMIT_DEFAULT_LIMIT_OAUTH_CLIENT: number;
|
||||
RATE_LIMIT_DEFAULT_LIMIT_ACCESS_TOKEN: number;
|
||||
RATE_LIMIT_DEFAULT_LIMIT: number;
|
||||
RATE_LIMIT_DEFAULT_BLOCK_DURATION_MS: number;
|
||||
};
|
||||
|
||||
@@ -22,18 +22,23 @@ const rateLimitSchema = z.object({
|
||||
ttl: z.number(),
|
||||
blockDuration: z.number(),
|
||||
});
|
||||
|
||||
type RateLimitType = z.infer<typeof rateLimitSchema>;
|
||||
|
||||
const rateLimitsSchema = z.array(rateLimitSchema);
|
||||
|
||||
const sixtySecondsMs = 60 * 1000;
|
||||
|
||||
@Injectable()
|
||||
export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
private logger = new Logger("CustomThrottlerGuard");
|
||||
|
||||
private defaultTttl = Number(getEnv("RATE_LIMIT_DEFAULT_TTL_MS", 60 * 1000));
|
||||
private defaultTttl = Number(getEnv("RATE_LIMIT_DEFAULT_TTL_MS", sixtySecondsMs));
|
||||
|
||||
private defaultLimitApiKey = Number(getEnv("RATE_LIMIT_DEFAULT_LIMIT_API_KEY", 120));
|
||||
private defaultLimitOAuthClient = Number(getEnv("RATE_LIMIT_DEFAULT_LIMIT_OAUTH_CLIENT", 500));
|
||||
private defaultLimitAccessToken = Number(getEnv("RATE_LIMIT_DEFAULT_LIMIT_ACCESS_TOKEN", 500));
|
||||
private defaultLimit = Number(getEnv("RATE_LIMIT_DEFAULT_LIMIT", 120));
|
||||
private defaultBlockDuration = Number(getEnv("RATE_LIMIT_DEFAULT_BLOCK_DURATION_MS", 60 * 1000));
|
||||
|
||||
private defaultBlockDuration = Number(getEnv("RATE_LIMIT_DEFAULT_BLOCK_DURATION_MS", sixtySecondsMs));
|
||||
|
||||
constructor(
|
||||
options: ThrottlerModuleOptions,
|
||||
@@ -51,6 +56,11 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const response = context.switchToHttp().getResponse<Response>();
|
||||
const tracker = await this.getTracker(request);
|
||||
this.logger.log(
|
||||
`Tracker "${tracker}" generated based on: Bearer token "${request.get(
|
||||
"Authorization"
|
||||
)}", OAuth client ID "${request.get(X_CAL_CLIENT_ID)}" and IP "${request.ip}"`
|
||||
);
|
||||
|
||||
if (tracker.startsWith("api_key_")) {
|
||||
return this.handleApiKeyRequest(tracker, response);
|
||||
@@ -78,7 +88,10 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
}
|
||||
|
||||
private async handleNonApiKeyRequest(tracker: string, response: Response): Promise<boolean> {
|
||||
const rateLimit = this.getDefaultRateLimit();
|
||||
const rateLimit = this.getDefaultRateLimit(tracker);
|
||||
this.logger.log(`Tracker "${tracker}" uses default rate limits because it is not tracking api key:
|
||||
${JSON.stringify(rateLimit, null, 2)}
|
||||
`);
|
||||
|
||||
const { isBlocked } = await this.incrementRateLimit(tracker, rateLimit, response);
|
||||
if (isBlocked) {
|
||||
@@ -88,17 +101,25 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
return true;
|
||||
}
|
||||
|
||||
private getDefaultRateLimit() {
|
||||
private getDefaultRateLimit(tracker: string) {
|
||||
return {
|
||||
name: "default",
|
||||
limit: this.getDefaultLimit(),
|
||||
limit: this.getDefaultLimit(tracker),
|
||||
ttl: this.getDefaultTtl(),
|
||||
blockDuration: this.getDefaultBlockDuration(),
|
||||
};
|
||||
}
|
||||
|
||||
getDefaultLimit() {
|
||||
return this.defaultLimit;
|
||||
getDefaultLimit(tracker: string) {
|
||||
if (tracker.startsWith("api_key_")) {
|
||||
return this.defaultLimitApiKey;
|
||||
} else if (tracker.startsWith("oauth_client_")) {
|
||||
return this.defaultLimitOAuthClient;
|
||||
} else if (tracker.startsWith("access_token_")) {
|
||||
return this.defaultLimitAccessToken;
|
||||
} else {
|
||||
return this.defaultLimit;
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultTtl() {
|
||||
@@ -114,6 +135,9 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
|
||||
const cachedRateLimits = await this.storageService.redis.get(cacheKey);
|
||||
if (cachedRateLimits) {
|
||||
this.logger.log(`Tracker "${tracker}" rate limits retrieved from redis cache:
|
||||
${cachedRateLimits}
|
||||
`);
|
||||
return rateLimitsSchema.parse(JSON.parse(cachedRateLimits));
|
||||
}
|
||||
|
||||
@@ -133,11 +157,18 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
select: { name: true, limit: true, ttl: true, blockDuration: true },
|
||||
});
|
||||
|
||||
if (!rateLimits || rateLimits.length === 0) {
|
||||
rateLimits = [this.getDefaultRateLimit()];
|
||||
if (rateLimits) {
|
||||
this.logger.log(`Tracker "${tracker}" rate limits retrieved from database:
|
||||
${JSON.stringify(rateLimits, null, 2)}`);
|
||||
}
|
||||
|
||||
await this.storageService.redis.setex(cacheKey, 3600, JSON.stringify(rateLimits));
|
||||
if (!rateLimits || rateLimits.length === 0) {
|
||||
rateLimits = [this.getDefaultRateLimit(tracker)];
|
||||
this.logger.log(`Tracker "${tracker}" rate limits not found in database. Using default rate limits:
|
||||
${JSON.stringify(rateLimits, null, 2)}`);
|
||||
}
|
||||
|
||||
await this.storageService.redis.set(cacheKey, JSON.stringify(rateLimits), "EX", 3600);
|
||||
|
||||
return rateLimits;
|
||||
}
|
||||
@@ -163,7 +194,15 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
);
|
||||
response.setHeader(`X-RateLimit-Reset-${nameFirstUpper}`, timeToBlockExpire || timeToExpire);
|
||||
|
||||
this.logger.log(`Rate limit for ${tracker} incremented. Remaining: ${limit - totalHits}`);
|
||||
this.logger.log(
|
||||
`Tracker "${tracker}" rate limit "${name}" incremented. isBlocked ${isBlocked}, totalHits ${totalHits}, timeToExpire ${timeToExpire}, timeToBlockExpire ${timeToBlockExpire}`
|
||||
);
|
||||
this.logger.log(
|
||||
`Tracker "${tracker}" rate limit "${name}" response headers:
|
||||
X-RateLimit-Limit-${nameFirstUpper}: ${limit},
|
||||
X-RateLimit-Remaining-${nameFirstUpper}: ${timeToBlockExpire ? 0 : Math.max(0, limit - totalHits)},
|
||||
X-RateLimit-Reset-${nameFirstUpper}: ${timeToBlockExpire || timeToExpire}`
|
||||
);
|
||||
|
||||
return { isBlocked };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ConferencingController } from "@/modules/conferencing/controllers/conferencing.controller";
|
||||
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
|
||||
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
||||
import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [
|
||||
ConferencingService,
|
||||
ConferencingRepository,
|
||||
GoogleMeetService,
|
||||
CredentialsRepository,
|
||||
UsersRepository,
|
||||
],
|
||||
exports: [],
|
||||
controllers: [ConferencingController],
|
||||
})
|
||||
export class ConferencingModule {}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { ConferencingAppsOutputDto } from "@/modules/conferencing/outputs/get-conferencing-apps.output";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import {
|
||||
ERROR_STATUS,
|
||||
GOOGLE_CALENDAR_ID,
|
||||
GOOGLE_CALENDAR_TYPE,
|
||||
GOOGLE_MEET,
|
||||
GOOGLE_MEET_TYPE,
|
||||
SUCCESS_STATUS,
|
||||
} from "@calcom/platform-constants";
|
||||
import { ApiErrorResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("Conferencing Endpoints", () => {
|
||||
describe("conferencing controller e2e tests", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let credentialsRepositoryFixture: CredentialsRepositoryFixture;
|
||||
|
||||
const userEmail = "conferencing-controller-user-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should get all the conferencing apps of the auth user", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/conferencing`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<ConferencingAppsOutputDto[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to connect google meet if google calendar is not connected ", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/conferencing/google-meet/connect`)
|
||||
.expect(400)
|
||||
.then(async () => {
|
||||
await credentialsRepositoryFixture.create(GOOGLE_CALENDAR_TYPE, {}, user.id, GOOGLE_CALENDAR_ID);
|
||||
});
|
||||
});
|
||||
|
||||
it("should connect google meet if google calendar is connected ", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/conferencing/google-meet/connect`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<ConferencingAppsOutputDto[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set google meet as default conferencing app", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/conferencing/google-meet/default`)
|
||||
.expect(200)
|
||||
.then(async () => {
|
||||
const updatedUser = await userRepositoryFixture.get(user.id);
|
||||
|
||||
expect(updatedUser).toBeDefined();
|
||||
|
||||
if (updatedUser) {
|
||||
const metadata = updatedUser.metadata as { defaultConferencingApp?: { appSlug?: string } };
|
||||
expect(metadata?.defaultConferencingApp?.appSlug).toEqual(GOOGLE_MEET);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should get all the conferencing apps of the auth user, and contain google meet", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/conferencing`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<ConferencingAppsOutputDto[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const googleMeet = responseBody.data.find((app) => app.type === GOOGLE_MEET_TYPE);
|
||||
expect(googleMeet?.userId).toEqual(user.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should disconnect google meet", async () => {
|
||||
return request(app.getHttpServer()).delete(`/v2/conferencing/google-meet/disconnect`).expect(200);
|
||||
});
|
||||
|
||||
it("should get all the conferencing apps of the auth user, and not contain google meet", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/conferencing`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<ConferencingAppsOutputDto[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const googleMeet = responseBody.data.find((app) => app.type === GOOGLE_MEET_TYPE);
|
||||
expect(googleMeet).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import {
|
||||
ConferencingAppsOutputResponseDto,
|
||||
ConferencingAppOutputResponseDto,
|
||||
ConferencingAppsOutputDto,
|
||||
} from "@/modules/conferencing/outputs/get-conferencing-apps.output";
|
||||
import { SetDefaultConferencingAppOutputResponseDto } from "@/modules/conferencing/outputs/set-default-conferencing-app.output";
|
||||
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
||||
import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service";
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
UseGuards,
|
||||
Post,
|
||||
Param,
|
||||
BadRequestException,
|
||||
Delete,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
import { CONFERENCING_APPS, GOOGLE_MEET, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/conferencing",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@DocsTags("Conferencing")
|
||||
export class ConferencingController {
|
||||
private readonly logger = new Logger("Platform Gcal Provider");
|
||||
|
||||
constructor(
|
||||
private readonly conferencingService: ConferencingService,
|
||||
private readonly googleMeetService: GoogleMeetService
|
||||
) {}
|
||||
|
||||
@Post("/:app/connect")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Connect your conferencing application" })
|
||||
async connect(
|
||||
@GetUser("id") userId: number,
|
||||
@Param("app") app: string
|
||||
): Promise<ConferencingAppOutputResponseDto> {
|
||||
switch (app) {
|
||||
case GOOGLE_MEET:
|
||||
const credential = await this.googleMeetService.connectGoogleMeetApp(userId);
|
||||
|
||||
return { status: SUCCESS_STATUS, data: plainToInstance(ConferencingAppsOutputDto, credential) };
|
||||
|
||||
default:
|
||||
throw new BadRequestException(
|
||||
"Invalid conferencing app, available apps are: ",
|
||||
CONFERENCING_APPS.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "List your conferencing applications" })
|
||||
async listConferencingApps(@GetUser("id") userId: number): Promise<ConferencingAppsOutputResponseDto> {
|
||||
const conferencingApps = await this.conferencingService.getConferencingApps(userId);
|
||||
|
||||
const data = conferencingApps.map((conferencingApps) =>
|
||||
plainToInstance(ConferencingAppsOutputDto, conferencingApps)
|
||||
);
|
||||
|
||||
return { status: SUCCESS_STATUS, data };
|
||||
}
|
||||
|
||||
@Post("/:app/default")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Set your default conferencing application" })
|
||||
async default(
|
||||
@GetUser("id") userId: number,
|
||||
@Param("app") app: string
|
||||
): Promise<SetDefaultConferencingAppOutputResponseDto> {
|
||||
switch (app) {
|
||||
case GOOGLE_MEET:
|
||||
await this.googleMeetService.setDefault(userId);
|
||||
|
||||
return { status: SUCCESS_STATUS };
|
||||
|
||||
default:
|
||||
throw new BadRequestException(
|
||||
"Invalid conferencing app, available apps are: ",
|
||||
CONFERENCING_APPS.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Delete("/:app/disconnect")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Disconnect your conferencing application" })
|
||||
async disconnect(
|
||||
@GetUser("id") userId: number,
|
||||
@Param("app") app: string
|
||||
): Promise<ConferencingAppOutputResponseDto> {
|
||||
switch (app) {
|
||||
case GOOGLE_MEET:
|
||||
const credential = await this.googleMeetService.disconnectGoogleMeetApp(userId);
|
||||
|
||||
return { status: SUCCESS_STATUS, data: plainToInstance(ConferencingAppsOutputDto, credential) };
|
||||
|
||||
default:
|
||||
throw new BadRequestException(
|
||||
"Invalid conferencing app, available apps are: ",
|
||||
CONFERENCING_APPS.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsString, ValidateNested, IsEnum, IsNumber, IsOptional, IsBoolean } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, GOOGLE_MEET_TYPE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class ConferencingAppsOutputDto {
|
||||
@Expose()
|
||||
@IsNumber()
|
||||
@ApiProperty({ description: "Id of the conferencing app credentials" })
|
||||
id!: number;
|
||||
|
||||
@ApiProperty({ example: GOOGLE_MEET_TYPE, description: "Type of conferencing app" })
|
||||
@Expose()
|
||||
@IsString()
|
||||
type!: string;
|
||||
|
||||
@ApiProperty({ description: "Id of the user associated to the conferencing app" })
|
||||
@Expose()
|
||||
@IsNumber()
|
||||
userId!: number;
|
||||
|
||||
@ApiProperty({ example: true, description: "Whether if the connection is working or not." })
|
||||
@Expose()
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
invalid?: boolean | null;
|
||||
}
|
||||
|
||||
export class ConferencingAppsOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => ConferencingAppsOutputDto)
|
||||
data!: ConferencingAppsOutputDto[];
|
||||
}
|
||||
|
||||
export class ConferencingAppOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => ConferencingAppsOutputDto)
|
||||
data!: ConferencingAppsOutputDto;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class SetDefaultConferencingAppOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { GOOGLE_MEET_TYPE } from "@calcom/platform-constants";
|
||||
|
||||
@Injectable()
|
||||
export class ConferencingRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async findConferencingApps(userId: number) {
|
||||
return this.dbRead.prisma.credential.findMany({
|
||||
where: {
|
||||
userId,
|
||||
type: { endsWith: "_video" },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findGoogleMeet(userId: number) {
|
||||
return this.dbRead.prisma.credential.findFirst({
|
||||
where: { userId, type: GOOGLE_MEET_TYPE },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class ConferencingService {
|
||||
private logger = new Logger("ConferencingService");
|
||||
|
||||
constructor(private readonly conferencingRepository: ConferencingRepository) {}
|
||||
|
||||
async getConferencingApps(userId: number) {
|
||||
return this.conferencingRepository.findConferencingApps(userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { GOOGLE_CALENDAR_TYPE, GOOGLE_MEET_TYPE, GOOGLE_MEET } from "@calcom/platform-constants";
|
||||
|
||||
@Injectable()
|
||||
export class GoogleMeetService {
|
||||
private logger = new Logger("GoogleMeetService");
|
||||
|
||||
constructor(
|
||||
private readonly conferencingRepository: ConferencingRepository,
|
||||
private readonly credentialsRepository: CredentialsRepository,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
async connectGoogleMeetApp(userId: number) {
|
||||
const googleCalendar = await this.credentialsRepository.getByTypeAndUserId(GOOGLE_CALENDAR_TYPE, userId);
|
||||
|
||||
if (!googleCalendar) {
|
||||
throw new BadRequestException("Google Meet app requires a Google Calendar connection");
|
||||
}
|
||||
|
||||
if (googleCalendar.invalid) {
|
||||
throw new BadRequestException(
|
||||
"Google Meet app requires a valid Google Calendar connection, please reconnect Google Calendar."
|
||||
);
|
||||
}
|
||||
|
||||
const googleMeet = await this.conferencingRepository.findGoogleMeet(userId);
|
||||
|
||||
if (googleMeet) {
|
||||
throw new BadRequestException("Google Meet is already connected.");
|
||||
}
|
||||
|
||||
const googleMeetCredential = await this.credentialsRepository.upsertAppCredential(
|
||||
GOOGLE_MEET_TYPE,
|
||||
{},
|
||||
userId
|
||||
);
|
||||
|
||||
return googleMeetCredential;
|
||||
}
|
||||
|
||||
async disconnectGoogleMeetApp(userId: number) {
|
||||
const googleMeet = await this.conferencingRepository.findGoogleMeet(userId);
|
||||
|
||||
if (!googleMeet) {
|
||||
throw new BadRequestException("Google Meet is not connected.");
|
||||
}
|
||||
|
||||
const googleMeetCredential = await this.credentialsRepository.deleteUserCredentialById(
|
||||
userId,
|
||||
googleMeet.id
|
||||
);
|
||||
|
||||
return googleMeetCredential;
|
||||
}
|
||||
|
||||
async setDefault(userId: number) {
|
||||
const user = await this.usersRepository.setDefaultConferencingApp(userId, GOOGLE_MEET);
|
||||
const metadata = user.metadata as { defaultConferencingApp?: { appSlug?: string } };
|
||||
|
||||
if (metadata?.defaultConferencingApp?.appSlug !== GOOGLE_MEET) {
|
||||
throw new InternalServerErrorException("Could not set Google Meet as default conferencing app");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,12 @@ export class CredentialsRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUserCredentialById(userId: number, credentialId: number) {
|
||||
return await this.dbWrite.prisma.credential.delete({
|
||||
where: { id: credentialId, userId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type CredentialsWithUserEmail = Awaited<
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
|
||||
import { AtomsModule } from "@/modules/atoms/atoms.module";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { ConferencingModule } from "@/modules/conferencing/conferencing.module";
|
||||
import { DestinationCalendarsModule } from "@/modules/destination-calendars/destination-calendars.module";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
@@ -22,6 +23,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
|
||||
DestinationCalendarsModule,
|
||||
AtomsModule,
|
||||
StripeModule,
|
||||
ConferencingModule,
|
||||
],
|
||||
})
|
||||
export class EndpointsModule implements NestModule {
|
||||
|
||||
-2
@@ -52,8 +52,6 @@ Second, make sure that the logged in user has organizationId set to pass the Org
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
|
||||
@DocsExcludeController(getEnv("NODE_ENV") === "production")
|
||||
@DocsTags("OAuth - development only")
|
||||
export class OAuthClientsController {
|
||||
private readonly logger = new Logger("OAuthClientController");
|
||||
|
||||
|
||||
+11
-2
@@ -25,9 +25,11 @@ import {
|
||||
import {
|
||||
ApiTags as DocsTags,
|
||||
ApiExcludeController as DocsExcludeController,
|
||||
ApiExcludeEndpoint as DocsExcludeEndpoint,
|
||||
ApiOperation as DocsOperation,
|
||||
ApiOkResponse as DocsOkResponse,
|
||||
ApiBadRequestResponse as DocsBadRequestResponse,
|
||||
ApiHeader as DocsHeader,
|
||||
} from "@nestjs/swagger";
|
||||
import { Response as ExpressResponse } from "express";
|
||||
|
||||
@@ -37,8 +39,7 @@ import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
path: "/v2/oauth/:clientId",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@DocsExcludeController(getEnv("NODE_ENV") === "production")
|
||||
@DocsTags("OAuth - development only")
|
||||
@DocsTags("OAuth")
|
||||
export class OAuthFlowController {
|
||||
constructor(
|
||||
private readonly oauthClientRepository: OAuthClientRepository,
|
||||
@@ -62,6 +63,7 @@ export class OAuthFlowController {
|
||||
description:
|
||||
"Bad request if the OAuth client is not found, if the redirect URI is invalid, or if the user has already authorized the client.",
|
||||
})
|
||||
@DocsExcludeEndpoint(getEnv("NODE_ENV") === "production")
|
||||
async authorize(
|
||||
@Param("clientId") clientId: string,
|
||||
@Body() body: OAuthAuthorizeInput,
|
||||
@@ -108,6 +110,7 @@ export class OAuthFlowController {
|
||||
description:
|
||||
"Bad request if the authorization code is missing, invalid, or if the client ID and secret do not match.",
|
||||
})
|
||||
@DocsExcludeEndpoint(getEnv("NODE_ENV") === "production")
|
||||
async exchange(
|
||||
@Headers("Authorization") authorization: string,
|
||||
@Param("clientId") clientId: string,
|
||||
@@ -138,6 +141,12 @@ export class OAuthFlowController {
|
||||
@Post("/refresh")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@DocsTags("Managed users")
|
||||
@DocsHeader({
|
||||
name: X_CAL_SECRET_KEY,
|
||||
description: "OAuth client secret key.",
|
||||
required: true,
|
||||
})
|
||||
async refreshAccessToken(
|
||||
@Param("clientId") clientId: string,
|
||||
@Headers(X_CAL_SECRET_KEY) secretKey: string,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { IsString } from "class-validator";
|
||||
|
||||
export class RefreshTokenInput {
|
||||
@IsString()
|
||||
@DocsProperty({ description: "Managed user's refresh token." })
|
||||
refreshToken!: string;
|
||||
}
|
||||
|
||||
@@ -76,4 +76,8 @@ export class UpdateOrgTeamDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
readonly bookingLimits?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly includeManagedEventsInLimits?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { SlotsService } from "@/modules/slots/services/slots.service";
|
||||
import { Query, Body, Controller, Get, Delete, Post, Req, Res } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { ApiTags as DocsTags, ApiCreatedResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger";
|
||||
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
@@ -19,6 +19,21 @@ export class SlotsController {
|
||||
constructor(private readonly slotsService: SlotsService) {}
|
||||
|
||||
@Post("/reserve")
|
||||
@ApiCreatedResponse({
|
||||
description: "Successful response returning uid of reserved slot.",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
status: { type: "string", example: "success" },
|
||||
data: {
|
||||
type: "object",
|
||||
properties: {
|
||||
uid: { type: "string", example: "e2a7bcf9-cc7b-40a0-80d3-657d391775a6" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiOperation({ summary: "Reserve a slot" })
|
||||
async reserveSlot(
|
||||
@Body() body: ReserveSlotInput,
|
||||
@@ -35,6 +50,15 @@ export class SlotsController {
|
||||
}
|
||||
|
||||
@Delete("/selected-slot")
|
||||
@ApiOkResponse({
|
||||
description: "Response deleting reserved slot by uid.",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
status: { type: "string", example: "success" },
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiOperation({ summary: "Delete a selected slot" })
|
||||
async deleteSelectedSlot(
|
||||
@Query() params: RemoveSelectedSlotInput,
|
||||
@@ -50,6 +74,45 @@ export class SlotsController {
|
||||
}
|
||||
|
||||
@Get("/available")
|
||||
@ApiOkResponse({
|
||||
description: "Available time slots retrieved successfully",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
status: { type: "string", example: "success" },
|
||||
data: {
|
||||
type: "object",
|
||||
properties: {
|
||||
slots: {
|
||||
type: "object",
|
||||
additionalProperties: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
time: { type: "string", format: "date-time", example: "2024-09-25T08:00:00.000Z" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
example: {
|
||||
status: "success",
|
||||
data: {
|
||||
slots: {
|
||||
"2024-09-25": [{ time: "2024-09-25T08:00:00.000Z" }, { time: "2024-09-25T08:15:00.000Z" }],
|
||||
"2024-09-26": [
|
||||
{ time: "2024-09-26T08:00:00.000Z" },
|
||||
{ time: "2024-09-26T08:15:00.000Z" },
|
||||
{ time: "2024-09-26T08:30:00.000Z" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiOperation({ summary: "Get available slots" })
|
||||
async getAvailableSlots(
|
||||
@Query() query: GetAvailableSlotsInput,
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
BadRequestException,
|
||||
Headers,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { ApiTags as DocsTags, ApiOperation } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import { Request } from "express";
|
||||
import { stringify } from "querystring";
|
||||
@@ -40,6 +40,7 @@ export class StripeController {
|
||||
@Get("/connect")
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: "Get stripe connect URL" })
|
||||
async redirect(
|
||||
@Req() req: Request,
|
||||
@Headers("Authorization") authorization: string,
|
||||
@@ -72,6 +73,7 @@ export class StripeController {
|
||||
@Get("/save")
|
||||
@UseGuards()
|
||||
@Redirect(undefined, 301)
|
||||
@ApiOperation({ summary: "Save stripe credentials" })
|
||||
async save(
|
||||
@Query("state") state: string,
|
||||
@Query("code") code: string,
|
||||
@@ -95,6 +97,7 @@ export class StripeController {
|
||||
@Get("/check")
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: "Check stripe connection" })
|
||||
async check(@GetUser() user: UserWithProfile): Promise<StripCredentialsCheckOutputResponseDto> {
|
||||
return await this.stripeService.checkIfStripeAccountConnected(user.id);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import type { Profile, User, Team } from "@prisma/client";
|
||||
|
||||
export type UserWithProfile = User & {
|
||||
@@ -223,4 +223,29 @@ export class UsersRepository {
|
||||
});
|
||||
return profiles.map((profile) => profile.user);
|
||||
}
|
||||
|
||||
async setDefaultConferencingApp(userId: number, appSlug?: string, appLink?: string) {
|
||||
const user = await this.findById(userId);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException("user not found");
|
||||
}
|
||||
|
||||
return await this.dbWrite.prisma.user.update({
|
||||
data: {
|
||||
metadata:
|
||||
typeof user.metadata === "object"
|
||||
? {
|
||||
...user.metadata,
|
||||
defaultConferencingApp: {
|
||||
appSlug: appSlug,
|
||||
appLink: appLink,
|
||||
},
|
||||
}
|
||||
: {},
|
||||
},
|
||||
|
||||
where: { id: userId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@
|
||||
"@storybook/blocks": "^7.6.3",
|
||||
"@storybook/nextjs": "^7.6.3",
|
||||
"@storybook/preview-api": "^7.6.3",
|
||||
"next": "^13.5.4",
|
||||
"next": "^13.5.6",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"storybook-addon-rtl-direction": "^0.0.19"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"dependencies": {
|
||||
"highlight.js": "^11.6.0",
|
||||
"isarray": "2.0.5",
|
||||
"next": "^13.5.4",
|
||||
"next": "^13.5.6",
|
||||
"openapi-snippet": "^0.13.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -27,12 +27,12 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyContext = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const data = await getData(legacyContext);
|
||||
const form = "form" in data ? (data.form as { name?: string; description?: string }) : null;
|
||||
const formName = form?.name ?? "Routing Forms";
|
||||
const formDescription = form?.description ?? "";
|
||||
const formName = form?.name;
|
||||
const formDescription = form?.description;
|
||||
|
||||
return await _generateMetadata(
|
||||
() => formName,
|
||||
() => formDescription
|
||||
(t) => formName ?? t("routing_forms"),
|
||||
(t) => formDescription ?? t("routing_forms_description")
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ import CategoryPage, { type PageProps } from "~/apps/categories/[category]/categ
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => "Apps Store",
|
||||
() => "Connecting people, technology and the workplace."
|
||||
(t) => t("app_store"),
|
||||
(t) => t("app_store_description")
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import Page from "~/apps/categories/categories-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => "Apps Store",
|
||||
() => "Connecting people, technology and the workplace."
|
||||
(t) => t("app_store"),
|
||||
(t) => t("app_store_description")
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ const getData = withAppDirSsg<Y>(getStaticProps);
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "Bookings",
|
||||
() => "Create events to share for people to book on your calendar."
|
||||
(t) => t("bookings"),
|
||||
(t) => t("bookings_description")
|
||||
);
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import InsightsPage from "~/insights/insights-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "Insights",
|
||||
(t) => t("insights"),
|
||||
(t) => t("insights_subtitle")
|
||||
);
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import PlatformView from "~/settings/platform/platform-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => "Platform",
|
||||
() => "Manage everything related to platform."
|
||||
(t) => t("platform"),
|
||||
(t) => t("platform_description")
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
lang={locale}
|
||||
dir={direction}
|
||||
style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}
|
||||
suppressHydrationWarning
|
||||
data-nextjs-router="app">
|
||||
<head nonce={nonce}>
|
||||
{!!process.env.NEXT_PUBLIC_HEAD_SCRIPTS && (
|
||||
|
||||
+7
-1
@@ -1,5 +1,6 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { getFixedT } from "app/_utils";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
@@ -16,9 +17,14 @@ const Page = async () => {
|
||||
const session = await getServerSessionForAppDir();
|
||||
|
||||
const t = await getFixedT(session?.user.locale || "en");
|
||||
const revalidatePage = async () => {
|
||||
"use server";
|
||||
revalidatePath("settings/my-account/general");
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsHeader title={t("general")} description={t("general_description")} borderInShellHeader={true}>
|
||||
<GeneralQueryView />
|
||||
<GeneralQueryView revalidatePage={revalidatePage} />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
@@ -42,7 +42,7 @@ export default function EnterprisePage() {
|
||||
];
|
||||
return (
|
||||
<div>
|
||||
<ShellMain heading="Enterprise" subtitle={t("enterprise_description")}>
|
||||
<ShellMain heading={t("enterprise")} subtitle={t("enterprise_description")}>
|
||||
<UpgradeTip
|
||||
plan="enterprise"
|
||||
title={t("create_your_org")}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { DefaultSeo } from "next-seo";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
|
||||
@@ -598,9 +598,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
<td className="flex w-full flex-col flex-wrap items-end justify-end space-x-2 space-y-2 py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:flex-row sm:flex-nowrap sm:items-start sm:space-y-0 sm:pl-0">
|
||||
{isUpcoming && !isCancelled ? (
|
||||
<>
|
||||
{isPending && (userId === booking.user?.id || booking.isUserTeamAdminOrOwner) && (
|
||||
<TableActions actions={pendingActions} />
|
||||
)}
|
||||
{isPending && <TableActions actions={pendingActions} />}
|
||||
{isConfirmed && <TableActions actions={bookedActions} />}
|
||||
{isRejected && <div className="text-subtle text-sm">{t("rejected")}</div>}
|
||||
</>
|
||||
|
||||
@@ -232,7 +232,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left" />
|
||||
|
||||
<p className="text-emphasis mb-2 ml-1 mt-6 text-sm font-bold">{t("current_location")}:</p>
|
||||
<p className="text-emphasis mb-2 ml-1 text-sm">
|
||||
<p className="text-emphasis mb-2 ml-1 break-all text-sm">
|
||||
{getHumanReadableLocationValue(booking.location, t)}
|
||||
</p>
|
||||
<Form
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { emailSchema } from "@calcom/lib/emailSchema";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -37,7 +38,7 @@ const SecondaryEmailModal = ({
|
||||
const formMethods = useForm<FormValues>({
|
||||
resolver: zodResolver(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
email: emailSchema,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
@@ -24,7 +24,9 @@ export const OAuthClientsDropdown = ({
|
||||
{Array.isArray(oauthClients) && oauthClients.length > 0 ? (
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button color="secondary">{initialClientName}</Button>
|
||||
<Button color="secondary" EndIcon="chevron-down">
|
||||
{initialClientName}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{oauthClients.map((client) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from "zod";
|
||||
|
||||
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { emailRegex } from "@calcom/lib/emailSchema";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
||||
|
||||
@@ -39,7 +40,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
|
||||
username: z
|
||||
.string()
|
||||
.refine((val) => val.trim().length >= 1, { message: t("at_least_characters", { count: 1 }) }),
|
||||
email_address: z.string().email({ message: t("enter_valid_email") }),
|
||||
email_address: z.string().regex(emailRegex, { message: t("enter_valid_email") }),
|
||||
full_name: z.string().min(3, t("at_least_characters", { count: 3 })),
|
||||
password: z.string().superRefine((data, ctx) => {
|
||||
const isStrict = true;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { CronJob } from "cron";
|
||||
|
||||
async function fetchCron(endpoint: string) {
|
||||
const apiKey = process.env.CRON_API_KEY;
|
||||
|
||||
const res = await fetch(`http://localhost:3000/api${endpoint}?${apiKey}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${process.env.CRON_SECRET}`,
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
console.log(endpoint, json);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("⏳ Running cron endpoints");
|
||||
new CronJob(
|
||||
// Each 5 seconds
|
||||
"*/5 * * * * *",
|
||||
async function () {
|
||||
await Promise.allSettled([
|
||||
fetchCron("/tasks/cron"),
|
||||
// fetchCron("/cron/calVideoNoShowWebhookTriggers"),
|
||||
//
|
||||
// fetchCron("/tasks/cleanup"),
|
||||
]);
|
||||
},
|
||||
null,
|
||||
true,
|
||||
"America/Los_Angeles"
|
||||
);
|
||||
} catch (_err) {
|
||||
console.error("❌ ❌ ❌ Something went wrong ❌ ❌ ❌");
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,15 +1,9 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export function register() {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
});
|
||||
export async function register() {
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "nodejs") {
|
||||
await import("./sentry.server.config");
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === "edge") {
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
});
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "edge") {
|
||||
await import("./sentry.edge.config");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,10 +71,12 @@ const CustomI18nextProvider = (props: { children: React.ReactElement; i18n?: SSR
|
||||
**/
|
||||
|
||||
const session = useSession();
|
||||
const locale =
|
||||
session?.data?.user.locale ?? typeof window !== "undefined"
|
||||
? window.document.documentElement.lang || "en"
|
||||
const fallbackLocale =
|
||||
typeof window !== "undefined" && window.document.documentElement.lang
|
||||
? window.document.documentElement.lang
|
||||
: "en";
|
||||
const newLocale = typeof window !== "undefined" && window.calNewLocale ? window.calNewLocale : null;
|
||||
const locale = session?.data?.user.locale ?? newLocale ?? fallbackLocale;
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -139,10 +141,10 @@ const CalcomThemeProvider = (props: CalcomThemeProps) => {
|
||||
const embedNamespace = searchParams ? getEmbedNamespace(searchParams) : null;
|
||||
const isEmbedMode = typeof embedNamespace === "string";
|
||||
|
||||
const themeProviderProps = getThemeProviderProps({ props, isEmbedMode, embedNamespace });
|
||||
const { key, ...themeProviderProps } = getThemeProviderProps({ props, isEmbedMode, embedNamespace });
|
||||
|
||||
return (
|
||||
<ThemeProvider {...themeProviderProps}>
|
||||
<ThemeProvider key={key} {...themeProviderProps}>
|
||||
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
|
||||
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
|
||||
{typeof window !== "undefined" && !isEmbedMode && (
|
||||
|
||||
@@ -145,10 +145,10 @@ const CalcomThemeProvider = (props: CalcomThemeProps) => {
|
||||
const embedNamespace = getEmbedNamespace(props.router.query);
|
||||
const isEmbedMode = typeof embedNamespace === "string";
|
||||
|
||||
const themeProviderProps = getThemeProviderProps({ props, isEmbedMode, embedNamespace });
|
||||
const { key, ...themeProviderProps } = getThemeProviderProps({ props, isEmbedMode, embedNamespace });
|
||||
|
||||
return (
|
||||
<ThemeProvider {...themeProviderProps}>
|
||||
<ThemeProvider key={key} {...themeProviderProps}>
|
||||
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
|
||||
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
|
||||
{typeof window !== "undefined" && !isEmbedMode && (
|
||||
|
||||
@@ -127,6 +127,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
queryDuration,
|
||||
eventData.length
|
||||
),
|
||||
durationConfig: eventData.metadata?.multipleDuration ?? [],
|
||||
booking,
|
||||
user: name,
|
||||
slug,
|
||||
|
||||
@@ -6,20 +6,21 @@ import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiu
|
||||
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { emailSchema } from "@calcom/lib/emailSchema";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const checkValidEmail = (email: string) => z.string().email().safeParse(email).success;
|
||||
const checkValidEmail = (email: string) => emailSchema.safeParse(email).success;
|
||||
|
||||
const querySchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => val || ""),
|
||||
email: z.string().email().optional(),
|
||||
email: emailSchema.optional(),
|
||||
});
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
|
||||
@@ -14,6 +14,8 @@ import type { RouterOutputs } from "@calcom/trpc";
|
||||
|
||||
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
type: z.string().transform((s) => slugify(s)),
|
||||
slug: z.string().transform((s) => slugify(s)),
|
||||
@@ -23,17 +25,12 @@ const paramsSchema = z.object({
|
||||
// 1. Check if team exists, to show 404
|
||||
// 2. If rescheduling, get the booking details
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const session = await getServerSession(context);
|
||||
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
|
||||
const {
|
||||
rescheduleUid,
|
||||
duration: queryDuration,
|
||||
isInstantMeeting: queryIsInstantMeeting,
|
||||
email,
|
||||
} = context.query;
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const { req, params, query } = context;
|
||||
const session = await getServerSession({ req });
|
||||
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(params);
|
||||
const { rescheduleUid, isInstantMeeting: queryIsInstantMeeting, email } = query;
|
||||
const ssr = await ssrInit(context);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req, params?.orgSlug);
|
||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||
|
||||
if (!isOrgContext) {
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import {
|
||||
generateGuestMeetingTokenFromOwnerMeetingToken,
|
||||
setEnableRecordingUIForOrganizer,
|
||||
setEnableRecordingUIAndUserIdForOrganizer,
|
||||
updateMeetingTokenIfExpired,
|
||||
} from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getCalVideoReference } from "@calcom/features/get-cal-video-reference";
|
||||
@@ -78,21 +79,34 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
|
||||
const oldVideoReference = getCalVideoReference(bookingObj.references);
|
||||
|
||||
const endTime = new Date(booking.endTime);
|
||||
const fourteenDaysAfter = new Date(endTime.getTime() + 14 * 24 * 60 * 60 * 1000);
|
||||
const epochTimeFourteenDaysAfter = Math.floor(fourteenDaysAfter.getTime() / 1000);
|
||||
|
||||
const videoReferencePassword = await updateMeetingTokenIfExpired({
|
||||
bookingReferenceId: oldVideoReference.id,
|
||||
roomName: oldVideoReference.uid,
|
||||
meetingToken: oldVideoReference.meetingPassword,
|
||||
exp: epochTimeFourteenDaysAfter,
|
||||
});
|
||||
|
||||
// set meetingPassword for guests
|
||||
if (session?.user.id !== bookingObj.user?.id) {
|
||||
const guestMeetingPassword = await generateGuestMeetingTokenFromOwnerMeetingToken(
|
||||
oldVideoReference.meetingPassword
|
||||
videoReferencePassword,
|
||||
session?.user.id
|
||||
);
|
||||
|
||||
bookingObj.references.forEach((bookRef) => {
|
||||
bookRef.meetingPassword = guestMeetingPassword;
|
||||
});
|
||||
}
|
||||
// Only for backward compatibility for organizer
|
||||
// Only for backward compatibility and setting user id in particpants for organizer
|
||||
else {
|
||||
const meetingPassword = await setEnableRecordingUIForOrganizer(
|
||||
const meetingPassword = await setEnableRecordingUIAndUserIdForOrganizer(
|
||||
oldVideoReference.id,
|
||||
oldVideoReference.meetingPassword
|
||||
videoReferencePassword,
|
||||
session?.user.id
|
||||
);
|
||||
if (!!meetingPassword) {
|
||||
bookingObj.references.forEach((bookRef) => {
|
||||
|
||||
@@ -98,10 +98,12 @@ export default function Apps({ categories, appStore, userAdminTeams }: PageProps
|
||||
}
|
||||
|
||||
export const LayoutWrapper = (page: React.ReactElement) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Shell
|
||||
title="Apps Store"
|
||||
description="Create forms to direct attendees to the correct destinations."
|
||||
title={t("app_store")}
|
||||
description={t("app_store_description")}
|
||||
withoutMain={true}
|
||||
hideHeadingOnMobile>
|
||||
{page}
|
||||
|
||||
@@ -21,8 +21,8 @@ export default function Apps({ apps }: PageProps) {
|
||||
<Shell
|
||||
isPublic
|
||||
backPath="/apps"
|
||||
title="Apps Store"
|
||||
description="Connecting people, technology and the workplace."
|
||||
title={t("app_store")}
|
||||
description={t("app_store_description")}
|
||||
smallHeading
|
||||
heading={
|
||||
<>
|
||||
|
||||
@@ -15,12 +15,7 @@ export default function Apps({ categories }: PageProps) {
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
|
||||
return (
|
||||
<Shell
|
||||
isPublic
|
||||
large
|
||||
hideHeadingOnMobile
|
||||
title="Apps Store"
|
||||
description="Connecting people, technology and the workplace.">
|
||||
<Shell isPublic large hideHeadingOnMobile title={t("app_store")} description={t("app_store_description")}>
|
||||
<div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0">
|
||||
<Link
|
||||
href="/apps"
|
||||
|
||||
@@ -12,6 +12,7 @@ import { z } from "zod";
|
||||
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { HOSTED_CAL_FEATURES, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { emailRegex } from "@calcom/lib/emailSchema";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLastUsed, LastUsed } from "@calcom/lib/hooks/useLastUsed";
|
||||
@@ -59,7 +60,7 @@ PageProps & WithNonceProps<{}>) {
|
||||
email: z
|
||||
.string()
|
||||
.min(1, `${t("error_required_field")}`)
|
||||
.email(`${t("enter_valid_email")}`),
|
||||
.regex(emailRegex, `${t("enter_valid_email")}`),
|
||||
...(!!totpEmail ? {} : { password: z.string().min(1, `${t("error_required_field")}`) }),
|
||||
})
|
||||
// Passthrough other fields like totpCode
|
||||
|
||||
@@ -213,11 +213,11 @@ export default function AvailabilityPage({ currentOrg }: PageProps) {
|
||||
<div>
|
||||
<Shell
|
||||
heading={t("availability")}
|
||||
title="Availability"
|
||||
description="Configure times when you are available for bookings."
|
||||
subtitle={t("configure_availability")}
|
||||
title={t("availability")}
|
||||
description={t("configure_availability")}
|
||||
hideHeadingOnMobile
|
||||
withoutMain={false}
|
||||
subtitle={t("configure_availability")}
|
||||
CTA={
|
||||
<div className="flex gap-2">
|
||||
<ToggleGroup
|
||||
|
||||
@@ -151,8 +151,8 @@ export default function Bookings() {
|
||||
hideHeadingOnMobile
|
||||
heading={t("bookings")}
|
||||
subtitle={t("bookings_description")}
|
||||
title="Bookings"
|
||||
description="Create events to share for people to book on your calendar.">
|
||||
title={t("bookings")}
|
||||
description={t("bookings_description")}>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row flex-wrap justify-between">
|
||||
<HorizontalTabs tabs={tabs} />
|
||||
|
||||
@@ -161,7 +161,11 @@ export default function Success(props: PageProps) {
|
||||
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
|
||||
const [comment, setComment] = useState("");
|
||||
const parsedRating = rating ? parseInt(rating, 10) : 3;
|
||||
const currentUserEmail = searchParams?.get("cancelledBy") ?? session?.user?.email ?? undefined;
|
||||
const currentUserEmail =
|
||||
searchParams?.get("rescheduledBy") ??
|
||||
searchParams?.get("cancelledBy") ??
|
||||
session?.user?.email ??
|
||||
undefined;
|
||||
|
||||
const defaultRating = isNaN(parsedRating) ? 3 : parsedRating > 5 ? 5 : parsedRating < 1 ? 1 : parsedRating;
|
||||
const [rateValue, setRateValue] = useState<number>(defaultRating);
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function Type({
|
||||
entity,
|
||||
duration,
|
||||
hashedLink,
|
||||
durationConfig,
|
||||
}: PageProps) {
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
@@ -35,6 +36,7 @@ export default function Type({
|
||||
entity={entity}
|
||||
duration={duration}
|
||||
hashedLink={hashedLink}
|
||||
durationConfig={durationConfig}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,6 @@ import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsF
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { parseEventTypeColor } from "@calcom/lib";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
@@ -250,6 +249,7 @@ export const InfiniteEventTypeList = ({
|
||||
const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = useState<SchedulingType | null>(
|
||||
null
|
||||
);
|
||||
const [privateLinkCopyIndices, setPrivateLinkCopyIndices] = useState<Record<string, number>>({});
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
const mutation = trpc.viewer.eventTypeOrder.useMutation({
|
||||
@@ -443,8 +443,11 @@ export const InfiniteEventTypeList = ({
|
||||
return page?.eventTypes?.map((type, index) => {
|
||||
const embedLink = `${group.profile.slug}/${type.slug}`;
|
||||
const calLink = `${bookerUrl}/${embedLink}`;
|
||||
const isPrivateURLEnabled = type.hashedLink?.link;
|
||||
const placeholderHashedLink = `${WEBSITE_URL}/d/${type.hashedLink?.link}/${type.slug}`;
|
||||
const isPrivateURLEnabled =
|
||||
type.hashedLink && type.hashedLink.length > 0
|
||||
? type.hashedLink[privateLinkCopyIndices[type.slug] ?? 0]?.link
|
||||
: "";
|
||||
const placeholderHashedLink = `${bookerUrl}/d/${isPrivateURLEnabled}/${type.slug}`;
|
||||
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
|
||||
const isChildrenManagedEventType =
|
||||
type.metadata?.managedEventConfig !== undefined &&
|
||||
@@ -544,6 +547,11 @@ export const InfiniteEventTypeList = ({
|
||||
onClick={() => {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
setPrivateLinkCopyIndices((prev) => {
|
||||
const prevIndex = prev[type.slug] ?? 0;
|
||||
prev[type.slug] = (prevIndex + 1) % type.hashedLink.length;
|
||||
return prev;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -53,10 +53,10 @@ export default function InsightsPage() {
|
||||
<div>
|
||||
<Shell
|
||||
withoutMain={false}
|
||||
heading="Insights"
|
||||
heading={t("insights")}
|
||||
subtitle={t("insights_subtitle")}
|
||||
title="Insights"
|
||||
description="View booking insights across your events.">
|
||||
title={t("insights")}
|
||||
description={t("insights_subtitle")}>
|
||||
<UpgradeTip
|
||||
plan="team"
|
||||
title={t("make_informed_decisions")}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Shell, { MobileNavigationMoreItems } from "@calcom/features/shell/Shell";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { MobileNavigationMoreItems } from "@calcom/features/shell/navigation/Navigation";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
export default function MorePage() {
|
||||
|
||||
@@ -192,15 +192,16 @@ const AppearanceView = ({
|
||||
</div>
|
||||
<Form
|
||||
form={userAppThemeFormMethods}
|
||||
handleSubmit={(values) => {
|
||||
handleSubmit={({ appTheme }) => {
|
||||
if (appTheme === "system") appTheme = null;
|
||||
mutation.mutate({
|
||||
appTheme: values.appTheme === "" ? null : values.appTheme,
|
||||
appTheme,
|
||||
});
|
||||
}}>
|
||||
<div className="border-subtle flex flex-col justify-between border-x px-6 py-8 sm:flex-row">
|
||||
<ThemeLabel
|
||||
variant="system"
|
||||
value={undefined}
|
||||
value="system"
|
||||
label={t("theme_system")}
|
||||
defaultChecked={user.appTheme === null}
|
||||
register={userAppThemeFormMethods.register}
|
||||
@@ -225,6 +226,7 @@ const AppearanceView = ({
|
||||
</div>
|
||||
<SectionBottomActions className="mb-6" align="end">
|
||||
<Button
|
||||
loading={mutation.isPending}
|
||||
disabled={isUserAppThemeSubmitting || !isUserAppThemeDirty}
|
||||
type="submit"
|
||||
data-testid="update-app-theme-btn"
|
||||
@@ -244,17 +246,21 @@ const AppearanceView = ({
|
||||
</div>
|
||||
<Form
|
||||
form={userThemeFormMethods}
|
||||
handleSubmit={(values) => {
|
||||
handleSubmit={({ theme }) => {
|
||||
if (theme === "light" || theme === "dark") {
|
||||
mutation.mutate({
|
||||
theme,
|
||||
});
|
||||
return;
|
||||
}
|
||||
mutation.mutate({
|
||||
// Radio values don't support null as values, therefore we convert an empty string
|
||||
// back to null here.
|
||||
theme: values.theme === "" ? null : values.theme,
|
||||
theme: null,
|
||||
});
|
||||
}}>
|
||||
<div className="border-subtle flex flex-col justify-between border-x px-6 py-8 sm:flex-row">
|
||||
<ThemeLabel
|
||||
variant="system"
|
||||
value={undefined}
|
||||
value="system"
|
||||
label={t("theme_system")}
|
||||
defaultChecked={user.theme === null}
|
||||
register={userThemeFormMethods.register}
|
||||
|
||||
@@ -58,7 +58,6 @@ const ConferencingView = () => {
|
||||
query={query}
|
||||
customLoader={<SkeletonLoader />}
|
||||
success={({ data }) => {
|
||||
console.log(data);
|
||||
if (!data.items.length) {
|
||||
return (
|
||||
<EmptyScreen
|
||||
|
||||
@@ -68,9 +68,14 @@ interface GeneralViewProps {
|
||||
localeProp: string;
|
||||
user: RouterOutputs["viewer"]["me"];
|
||||
travelSchedules: RouterOutputs["viewer"]["getTravelSchedules"];
|
||||
revalidatePage: GeneralQueryViewProps["revalidatePage"];
|
||||
}
|
||||
|
||||
const GeneralQueryView = () => {
|
||||
type GeneralQueryViewProps = {
|
||||
revalidatePage: () => Promise<void>;
|
||||
};
|
||||
|
||||
const GeneralQueryView = ({ revalidatePage }: GeneralQueryViewProps) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const { data: user, isPending } = trpc.viewer.me.useQuery();
|
||||
@@ -82,10 +87,17 @@ const GeneralQueryView = () => {
|
||||
if (!user) {
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
return <GeneralView user={user} travelSchedules={travelSchedules || []} localeProp={user.locale} />;
|
||||
return (
|
||||
<GeneralView
|
||||
user={user}
|
||||
travelSchedules={travelSchedules || []}
|
||||
localeProp={user.locale}
|
||||
revalidatePage={revalidatePage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const GeneralView = ({ localeProp, user, travelSchedules }: GeneralViewProps) => {
|
||||
const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: GeneralViewProps) => {
|
||||
const utils = trpc.useContext();
|
||||
const {
|
||||
t,
|
||||
@@ -105,6 +117,7 @@ const GeneralView = ({ localeProp, user, travelSchedules }: GeneralViewProps) =>
|
||||
if (res.locale) {
|
||||
window.calNewLocale = res.locale;
|
||||
}
|
||||
await revalidatePage();
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
@@ -177,7 +190,7 @@ const GeneralView = ({ localeProp, user, travelSchedules }: GeneralViewProps) =>
|
||||
<div>
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={(values) => {
|
||||
handleSubmit={async (values) => {
|
||||
setIsUpdateBtnLoading(true);
|
||||
mutation.mutate({
|
||||
...values,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { z } from "zod";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
||||
import { emailSchema } from "@calcom/lib/emailSchema";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
@@ -523,12 +524,12 @@ const ProfileForm = ({
|
||||
.max(FULL_NAME_LENGTH_MAX_LIMIT, {
|
||||
message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }),
|
||||
}),
|
||||
email: z.string().email(),
|
||||
email: emailSchema,
|
||||
bio: z.string(),
|
||||
secondaryEmails: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
email: z.string().email(),
|
||||
email: emailSchema,
|
||||
emailVerified: z.union([z.string(), z.null()]).optional(),
|
||||
emailPrimary: z.boolean().optional(),
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import Shell from "@calcom/features/shell/Shell";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { PlatformPricing } from "@calcom/web/components/settings/platform/pricing/platform-pricing/index";
|
||||
|
||||
import NoPlatformPlan from "@components/settings/platform/dashboard/NoPlatformPlan";
|
||||
import { useGetUserAttributes } from "@components/settings/platform/hooks/useGetUserAttributes";
|
||||
@@ -23,13 +24,33 @@ export default function PlatformBillingUpgrade() {
|
||||
const onContactSupportClick = async () => {
|
||||
await open();
|
||||
};
|
||||
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, userBillingData } = useGetUserAttributes();
|
||||
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, userBillingData, isPaidUser, userOrgId } =
|
||||
useGetUserAttributes();
|
||||
|
||||
if (isUserLoading || (isUserBillingDataLoading && !userBillingData)) {
|
||||
return <div className="m-5">Loading...</div>;
|
||||
}
|
||||
|
||||
if (!isPlatformUser) return <NoPlatformPlan />;
|
||||
if (isPlatformUser && !isPaidUser)
|
||||
return (
|
||||
<PlatformPricing
|
||||
teamId={userOrgId}
|
||||
heading={
|
||||
<div className="mb-5 text-center text-2xl font-semibold">
|
||||
<h1>Subscribe to Platform</h1>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!isPlatformUser)
|
||||
return (
|
||||
<div>
|
||||
<Shell isPlatformUser={true} hideHeadingOnMobile withoutMain={false} SidebarContainer={<></>}>
|
||||
<NoPlatformPlan />
|
||||
</Shell>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
+8
@@ -69,6 +69,14 @@ export default function EditOAuthClientWebhooks() {
|
||||
value: WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED,
|
||||
label: "recording_transcription_generated",
|
||||
},
|
||||
{
|
||||
value: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||
label: "after_hosts_cal_video_no_show",
|
||||
},
|
||||
{
|
||||
value: WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||
label: "after_guests_cal_video_no_show",
|
||||
},
|
||||
]}
|
||||
onSubmit={async (data) => {
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { showToast } from "@calcom/ui";
|
||||
|
||||
import {
|
||||
@@ -22,6 +23,7 @@ import { PlatformPricing } from "@components/settings/platform/pricing/platform-
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export default function Platform() {
|
||||
const { t } = useLocale();
|
||||
const [initialClientId, setInitialClientId] = useState("");
|
||||
const [initialClientName, setInitialClientName] = useState("");
|
||||
|
||||
@@ -75,11 +77,12 @@ export default function Platform() {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div>
|
||||
<Shell
|
||||
heading="Platform"
|
||||
title="Platform"
|
||||
heading={t("platform")}
|
||||
subtitle={t("platform_description")}
|
||||
title={t("platform")}
|
||||
description={t("platform_description")}
|
||||
hideHeadingOnMobile
|
||||
withoutMain={false}
|
||||
subtitle="Manage everything related to platform."
|
||||
isPlatformUser={true}>
|
||||
<HelpCards />
|
||||
<OAuthClientsList oauthClients={data} isDeleting={isDeleting} handleDelete={handleDelete} />
|
||||
|
||||
@@ -19,10 +19,10 @@ function Teams(props: PageProps) {
|
||||
<Shell
|
||||
withoutMain={false}
|
||||
heading={t("teams")}
|
||||
title="Teams"
|
||||
description="Create and manage teams to use collaborative features."
|
||||
hideHeadingOnMobile
|
||||
subtitle={t("create_manage_teams_collaborative")}
|
||||
title={t("teams")}
|
||||
description={t("create_manage_teams_collaborative")}
|
||||
hideHeadingOnMobile
|
||||
CTA={
|
||||
(!user.organizationId || user.organization.isOrgAdmin) && (
|
||||
<Button
|
||||
|
||||
@@ -178,6 +178,7 @@ const nextConfig = {
|
||||
serverComponentsExternalPackages: ["next-i18next"],
|
||||
optimizePackageImports: ["@calcom/ui"],
|
||||
instrumentationHook: true,
|
||||
serverActions: true,
|
||||
},
|
||||
i18n: {
|
||||
...i18n,
|
||||
@@ -291,8 +292,8 @@ const nextConfig = {
|
||||
destination: "/apps/routing-forms/routing-link/:formQuery*",
|
||||
},
|
||||
{
|
||||
source: "/router",
|
||||
destination: "/apps/routing-forms/router",
|
||||
source: "/router/:path*",
|
||||
destination: "/apps/routing-forms/router/:path*",
|
||||
},
|
||||
{
|
||||
source: "/success/:path*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "4.5.8",
|
||||
"version": "4.6.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -8,6 +8,7 @@
|
||||
"analyze:browser": "BUNDLE_ANALYZE=browser next build",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
||||
"dev": "next dev",
|
||||
"dev:cron": "ts-node cron-tester.ts",
|
||||
"dev-https": "NODE_TLS_REJECT_UNAUTHORIZED=0 next dev --experimental-https",
|
||||
"dx": "yarn dev",
|
||||
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
||||
@@ -79,6 +80,7 @@
|
||||
"bcp-47-match": "^2.0.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"classnames": "^2.3.1",
|
||||
"dompurify": "^3.1.7",
|
||||
"dotenv-cli": "^6.0.0",
|
||||
"entities": "^4.4.0",
|
||||
"eslint-config-next": "^13.2.1",
|
||||
@@ -97,7 +99,7 @@
|
||||
"memory-cache": "^0.2.0",
|
||||
"micro": "^10.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"next": "^13.5.4",
|
||||
"next": "^13.5.6",
|
||||
"next-auth": "^4.22.1",
|
||||
"next-axiom": "^0.17.0",
|
||||
"next-collect": "^0.2.1",
|
||||
@@ -105,6 +107,7 @@
|
||||
"next-seo": "^6.0.0",
|
||||
"next-themes": "^0.2.0",
|
||||
"nodemailer": "^6.7.8",
|
||||
"nuqs": "^1.20.0",
|
||||
"otplib": "^12.0.1",
|
||||
"qrcode": "^1.5.1",
|
||||
"raw-body": "^2.5.1",
|
||||
@@ -121,7 +124,6 @@
|
||||
"react-multi-email": "^0.5.3",
|
||||
"react-phone-input-2": "^2.15.1",
|
||||
"react-phone-number-input": "^3.2.7",
|
||||
"react-query": "^3.39.3",
|
||||
"react-schemaorg": "^2.0.0",
|
||||
"react-select": "^5.7.0",
|
||||
"react-timezone-select": "^1.4.0",
|
||||
@@ -151,6 +153,7 @@
|
||||
"@types/async": "^3.2.15",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/detect-port": "^1.3.2",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/glidejs__glide": "^3.4.2",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
@@ -170,6 +173,8 @@
|
||||
"@types/uuid": "8.3.1",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"cron": "^3.1.7",
|
||||
"deasync": "^0.1.30",
|
||||
"detect-port": "^1.3.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"module-alias": "^2.2.2",
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function Custom404() {
|
||||
|
||||
const links = [
|
||||
{
|
||||
title: "Enterprise",
|
||||
title: t("enterprise"),
|
||||
description: "Learn more about organizations and subdomains in our enterprise plan.",
|
||||
icon: "shield" as const,
|
||||
href: `${WEBSITE_URL}/enterprise`,
|
||||
|
||||
@@ -67,7 +67,39 @@ class MyDocument extends Document<Props> {
|
||||
nonce={nonce}
|
||||
id="newLocale"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `window.calNewLocale = "${newLocale}";`,
|
||||
__html: `
|
||||
window.calNewLocale = "${newLocale}";
|
||||
(function applyTheme() {
|
||||
try {
|
||||
const appTheme = localStorage.getItem('app-theme');
|
||||
if (!appTheme) return;
|
||||
|
||||
let bookingTheme, username;
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key.startsWith('booking-theme:')) {
|
||||
bookingTheme = localStorage.getItem(key);
|
||||
username = key.split("booking-theme:")[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const onReady = () => {
|
||||
const isBookingPage = username && window.location.pathname.slice(1).startsWith(username);
|
||||
|
||||
if (document.body) {
|
||||
document.body.classList.add(isBookingPage ? bookingTheme : appTheme);
|
||||
} else {
|
||||
requestAnimationFrame(onReady);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(onReady);
|
||||
} catch (e) {
|
||||
console.error('Error applying theme:', e);
|
||||
}
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/api/logo?type=apple-touch-icon" />
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { passwordResetRequest } from "@calcom/features/auth/lib/passwordResetRequest";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { emailSchema } from "@calcom/lib/emailSchema";
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const email = z
|
||||
.string()
|
||||
.email()
|
||||
.transform((val) => val.toLowerCase())
|
||||
.safeParse(req.body?.email);
|
||||
const email = emailSchema.transform((val) => val.toLowerCase()).safeParse(req.body?.email);
|
||||
|
||||
if (!email.success) {
|
||||
return res.status(400).json({ message: "email is required" });
|
||||
|
||||
@@ -3,6 +3,7 @@ import z from "zod";
|
||||
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
||||
import { emailRegex } from "@calcom/lib/emailSchema";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
@@ -14,7 +15,7 @@ const querySchema = z.object({
|
||||
.string()
|
||||
.refine((val) => val.trim().length >= 1, { message: "Please enter at least one character" }),
|
||||
full_name: z.string().min(3, "Please enter at least 3 characters"),
|
||||
email_address: z.string().email({ message: "Please enter a valid email" }),
|
||||
email_address: z.string().regex(emailRegex, { message: "Please enter a valid email" }),
|
||||
password: z.string().refine((val) => isPasswordValid(val.trim(), false, true), {
|
||||
message:
|
||||
"The password must be a minimum of 15 characters long containing at least one number and have a mixture of uppercase and lowercase letters",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments";
|
||||
import { TeamBilling } from "@calcom/ee/billing/teams";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const querySchema = z.object({
|
||||
@@ -32,6 +32,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
metadata: true,
|
||||
isOrganization: true,
|
||||
parentId: true,
|
||||
},
|
||||
skip: pageNumber * pageSize,
|
||||
take: pageSize,
|
||||
@@ -41,10 +44,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
break;
|
||||
}
|
||||
|
||||
for (const team of teams) {
|
||||
await updateQuantitySubscriptionFromStripe(team.id);
|
||||
await delay(100); // Adjust the delay as needed to avoid rate limiting
|
||||
}
|
||||
const teamsBilling = TeamBilling.initMany(teams);
|
||||
const teamBillingPromises = teamsBilling.map((teamBilling) => teamBilling.updateQuantity());
|
||||
await Promise.allSettled(teamBillingPromises);
|
||||
|
||||
pageNumber++;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export { default } from "@calcom/features/ee/billing/api/webhook";
|
||||
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import getRawBody from "raw-body";
|
||||
import z from "zod";
|
||||
|
||||
import { emailSchema } from "@calcom/lib/emailSchema";
|
||||
import { default as webPrisma } from "@calcom/prisma";
|
||||
|
||||
export const config = {
|
||||
@@ -13,7 +14,7 @@ export const config = {
|
||||
|
||||
const helpscoutRequestBodySchema = z.object({
|
||||
customer: z.object({
|
||||
email: z.string().email(),
|
||||
email: emailSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Meta } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/auth/setup-view";
|
||||
@@ -17,6 +19,8 @@ const Page = (props: PageProps) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps };
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user