Compare commits

..
94 Commits
Author SHA1 Message Date
GitHub Actions ca19aa00c1 chore: release v5.1.2
Post release / release (push) Canceled after 0s
2025-03-19 21:59:27 +00:00
Anik Dhabal Babu 467c979aa7 fix: updating the user does not invalidate/refresh the table (#20228) 2025-03-19 20:28:18 +00:00
Hariom Balhara 9f9bf10040 fix: A user exists with that username (#20217) 2025-03-19 14:34:33 +00:00
Lauris Skraucis 8102192987 refactor: v2 booking responses schema (#20030)
* refactor: when rescheduling fallback on attendee if responses invalid

* chore: submit auto generated docs

* fix: slots event type metadata parsing

* fix: booking responses without email with attendee phone

* refactor

* refactor: test

* remove unused import

* refactor

* test: slots variable duration
2025-03-19 16:32:00 +02:00
Pranav PatilandTushar Bhatt 232f9d288c fix: Loading indicator alignment in settings page (#20219)
* Fix: Loading indicator alignment in profile page

* Added animate spin class

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-19 14:31:05 +00:00
Anik Dhabal Babu 7dab100382 fix: unskip confirm step if there are multiple location (#20051) 2025-03-19 12:17:49 +00:00
Lauris Skraucis 91d4f1c219 feat: booker atom confirm and back button styling (#20162)
* feat: submitButtonDisabled Booker atom prop

* feat: confirmStep.confirmButton & backButton booker styles

* refactor: rename submitButtonDisabled to confirmButtonDisabled

* feat: implement this feature for skip confirm step

* refactor: naming
2025-03-19 11:30:48 +00:00
MorganandSomay Chauhan a64e7f9c2a fix: set default managed users calendars (#20208)
* fix: set default managed users calendars

* fix: module imports

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2025-03-19 09:22:28 +00:00
sean-brydonandKeith Williams 3c1297aa72 fix: calcom trpc sessio circle dep (#19893)
* fix trpc session circle dep

* fixes trpc session cirlce dep

* fix relative imports

* fix more imports to use types and not trpc

* fix exports

* Fixed types

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-19 05:50:22 -03:00
Vijay 96f7a01c5f fix: few flaky tests in ooo and bookings-list (#20170)
* fix flaky tests

* chore
2025-03-19 08:32:47 +00:00
Udit TakkarandTushar Bhatt a4a05878ef fix: use correct skeleton loader (#20197)
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-19 08:07:22 +00:00
Keith WilliamsandhbjORbj 352d68ae1c perf: Remove app-store reference from @calcom/ui (#20188)
* perf: Remove app-store reference from @calcom/ui

* Fixing types

* Fixed casing

* fix type check

* refactor

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-19 05:04:02 -03:00
Tushar Bhatt ffd034679c fix: team-update-on-eventTypes-page (#20152) 2025-03-19 07:28:18 +00:00
Morgan d64ae3c901 fix: missing booking reminder reference in cron (#20200) 2025-03-19 07:27:52 +00:00
Hariom Balhara 7250e22257 fix salesforce query (#20196) 2025-03-19 09:23:12 +02:00
Udit Takkar f459e5d9c4 fix: use correct logo (#20194) 2025-03-19 06:29:07 +00:00
Tushar Bhatt 30bb0f1383 fix: logo-sync-issue (#20190) 2025-03-19 04:44:55 +00:00
Abdullah Arif 24c993a3d2 fix(createButton): add scrolling to empty webhook dropdown (#20191)
Added a scroll-bar class and set a max height on DropdownMenuContent to ensure large dropdowns are scrollable, improving the user experience by making all items accessible without expanding beyond the viewport.
2025-03-19 09:54:09 +05:30
GitHub Actions aa4276be5b chore: release v5.1.1
Post release / release (push) Canceled after 0s
2025-03-18 20:45:23 +00:00
Somay ChauhanandKeith Williams 413aba6e52 fix: add documentation to the possible values of :app in conferencing controller (#20164)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-18 17:41:35 -03:00
sean-brydon c2547acb3d fix eslint error (#20127) 2025-03-18 20:21:08 +00:00
Benny Joo cc1b6bac4a perf: Remove QueryCell from availability view (#20176) 2025-03-18 17:05:02 -03:00
Udit Takkar d5d90a8b87 fix: sms formatting (#18647) 2025-03-18 16:59:13 -03:00
Joe Au-YeungandKeith Williams 40333c81c5 chore: Add logging to Salesforce (#20167)
* Remove unused param

* Add log when no contacts are created

* Replace console.log with logger

* Add logging around `buildRecordUpdatePayload`

* Add details if field isn't being caught

* Revert change

* Type fixes

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-18 19:51:02 +00:00
Tushar Bhatt ff92979311 fix: spacing (#20172) 2025-03-18 17:37:24 +00:00
Anik Dhabal Babu f63edef48d fix: flaky e2e tests (#20168)
* fix: flaky e2e tests

* update

* fix
2025-03-18 22:30:00 +05:30
Rajiv Sahal 37e56ebbf9 dont auto accept user into an org when creating platform org (#20125) 2025-03-18 16:12:26 +00:00
07c38f1f33 test: Add tests for Booker component (#19051)
* feat: Don't allow unavailable slot's booking form to be visible - Send back to slots listing

* Improvements

* feat: Add env variables and strategies to prevent booking failures

- Added new environment variables for configuring slot reservation and availability checks
- Implemented strategies to handle concurrent bookings and slot availability
- Updated Booker component and related files to support new reservation and slot checking mechanisms
- Added README with detailed explanation of booking prevention strategies
- Configured dynamic intervals for slot and reservation queries

* docs: Update Booker README with detailed slot reservation and availability explanation

- Clarified slot reservation behavior when multiple users access the same booking page
- Added details about `getSchedule` refetching strategies and timing
- Explained how slot availability is detected and communicated to users

* Support for skip confirmation form flow

* feat: Enhance slot availability and reservation mechanism

- Refactored slot availability checking to support multiple slots
- Added tentative selected timeslots to improve booking UX
- Updated trpc handler to check availability for multiple slots
- Introduced new store methods for managing tentative slot selections
- Improved slot reservation and availability status tracking

* refactor: Improve slot availability quick check mechanism

- Renamed and restructured slot availability check parameters
- Extracted quick availability checks into a separate hook
- Updated type definitions for slot status and availability checks
- Simplified slot availability logic in Booker and related components
- Maintained cached state for quick availability checks

* Add tests booker

* Support minimu booking check too

* change data-testid

* Rneame

* Fix race condition with uid cookie not set

* fix tests

* Remove unsded variable

* fixup! Merge branch 'main' into dont-allow-booking-form-with-unavaialble-slot

* Fix the unavailability bug when only seconds differ

* Handle column view that doesnt have selected date and also add tests for isTimeSlotAavailable

* Fi ts and unit tests

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-03-18 13:02:20 +01:00
Gene Parmesan Thomas 192238f4bf fix: UID cookie isn't accessible/created when in embed as it is third party context. (#19859)
* [CAL-5240] Embeds: UID cookie isn't accessible/created when in embed as it is third party context.

* comments
2025-03-18 16:22:55 +05:30
Lauris SkraucisandMorgan 94a26031a8 feat: v2 api phone based organization team event types (#20134)
* refactor: input event types service remove unused code

* feat: toggle if email field is hidden

* chore: ensure event type has email or attendee phone field required

* chore: ensure non-org team cant have phone only event type

* chore: ensure users cant have phone only event type

* chore: ensure org team can have phone only event type

* chore: bump libraries

* docs

* refactor: confirm failing test response message

* fix: unit test

* chore: default organizer booking location docs

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-03-18 11:34:35 +02:00
Joe Au-Yeung e9f20b1a0b fix: Salesforce - fallback to creating lead (#20137)
* Fallback creating lead

* Add tests

* Type fix

* Type fixes

* Type fix

* Fix tests
2025-03-18 14:29:09 +05:30
Morgan 7cb9e2b99b chore: create org membership should be an upsert apiv2 (#20129) 2025-03-17 21:07:32 +00:00
Morgan dd45d8cdbc fix: don't remind platform users in reminder cron (#20056) 2025-03-17 23:00:25 +02:00
Hariom Balhara 3a181f1dac fix: Salesforce Stricter Website querying to prevent wrong account match during contact owner check (#20133)
* Make website check stricter in contact ownership flow as well

* Add test
2025-03-17 17:34:36 -03:00
Morgan c3abfa51ba chore: stringify body for request interceptor/middleware (#20131)
* chore: stringify body for request interceptor/middleware

* rename jsonBody to jsonBodyString
2025-03-17 19:45:37 +00:00
Alex van Andel 6498af167a refactor: Delete dead code that is not safe to use (#20141) 2025-03-17 17:23:39 +00:00
Keith WilliamsandhbjORbj 30dce7bacd perf: Move apps procedures off of loggedInViewer (#20112)
* perf: Slim down loggedInViewer tRPC router

* Fixed trpc client calls for new routes

* Moved bookingUnconfirmedCount

* Moved getUserTopBanners to me router

* Moved shouldVerifyEmail to me router

* Moved i18n from public to its own router

* fix ssrInit

* fix ssrInit usage

* fix type check

* better naming

* fix

* fix

* Fix types

* Removed used of importHandler

* perf: Move apps procedures off of loggedInViewer

* Moved 2 more apps-related procedures

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-17 17:18:19 +00:00
Keith Williams 3be9e1bd38 perf: Remove slots router from publicViewer (#20128) 2025-03-17 09:28:40 -07:00
Lauris Skraucis db52f44293 fix: atoms making requests to customers' domains (#20084)
* refactor: centralize useSchedule hook with isInit

* refactor: centralize useSchedules hook with isInit

* refactor: useConnectedCalendars enabled if isInit

* chore: some type fixes

* chore: type fix

* fix: import paths

* driveby: oauth clients docs
2025-03-17 10:43:39 +02:00
Keith WilliamsandhbjORbj 20d6eb5200 perf: Slim down loggedInViewer tRPC router (#20111)
* perf: Slim down loggedInViewer tRPC router

* Fixed trpc client calls for new routes

* Moved bookingUnconfirmedCount

* Moved getUserTopBanners to me router

* Moved shouldVerifyEmail to me router

* Moved i18n from public to its own router

* fix ssrInit

* fix ssrInit usage

* fix type check

* better naming

* fix

* fix

* Fix types

* Removed used of importHandler

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-16 18:22:48 -07:00
Keith Williams 424d94f16d feat: Use Watchlist on sending email verifications (#20114) 2025-03-16 14:20:20 +00:00
GitHub Actions 73e7d6f986 chore: release v5.1.0
Post release / release (push) Canceled after 0s
2025-03-15 11:11:37 +00:00
Alex van Andel 145001d26e chore: Bump typescript version in @calcom/ui (#19766)
* chore: Bump typescript version in @calcom/ui

* Reduce required types from @calcom/types to just used ones
2025-03-15 06:43:05 -03:00
5d2d7ce979 fix: German translation for seats_availability text (#20086)
* Fix German translation for seats_availability text

* enhancements

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Tushar <tusharbhatt0135@gmail.com>
2025-03-15 08:12:04 +00:00
GitHub Actions 4dc21ab2b0 chore: release v5.0.20
Post release / release (push) Canceled after 0s
2025-03-14 19:07:01 +00:00
Anik Dhabal Babu df6ee73e05 fix: schedule.get unneccsary called from avaiablity view (#20094) 2025-03-14 18:31:35 +00:00
Keith Williams 587af33179 perf: Use built types for appRouter (#20083)
* perf: Use built types for appRouter

* Got it working just for server

* Added dependency to turbo.json
2025-03-14 10:42:45 -07:00
Morgan 3b381b037c fix: incorrect managed user username format on update (#20090) 2025-03-14 15:36:24 +00:00
0968fc56eb feat: add utm tracking on booking (#19965)
* add tracking to booking + pass to handleNewBooking

* save tracking to booking

* return undefined if no utm params

* add test for handleNewBooking

* fix type error in test

* refactor code

* code clean up

* code clean up

* add missing import

* fix type import

* add utm params to insights (#20063)

* add utm params to insights

* remove duplicates

* remove duplicate columns

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2025-03-14 08:21:55 -07:00
Lauris Skraucis 13044cb5b0 feat: v2 bookings createdAt filter (#20054)
* feat: add before and after updated filter to core handler

* feat: afterCreatedAt and beforeCreatedAt v2 bookings filters

* wip

* finish

* chore: bump libraries

* fix: tests

* refactor: implement feedback
2025-03-14 15:49:57 +02:00
Morgan 580b03f2b9 chore: handle conferencing callback missing state error (#20081) 2025-03-14 15:49:04 +02:00
amrit 543d4bb6b3 fix: invitati instead of inviati (#20085) 2025-03-14 17:57:19 +05:30
Sarvesh Bajaj 21d87d5a80 fix: No type errors in build (#20075) 2025-03-14 07:39:10 +00:00
Lauris Skraucis 194719e7ba docs: v2 set cal-api-version header automatically (#20042) 2025-03-14 07:37:14 +00:00
Benny JooandKeith Williams dc666fc770 perf: fix paths in tailwind preset + ignore node modules in Platform tailwind config (#20067)
* fix paths in packages/config/tailwind preset

* ignore node_modules in platform tailwind preset

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-14 04:18:16 -03:00
Morgan 50a76e6b27 fix: delegation credential not found (#20055) 2025-03-14 06:13:41 +00:00
Parteek malikandamrit be4d3170db fix: synced no-show state after mutation (#20015)
Co-authored-by: amrit <iamamrit27@gmail.com>
2025-03-14 05:49:59 +00:00
Anik Dhabal Babu 7b97002230 fix: flaky test in ooo e2e (#20073)
* fix: flaky test in ooo e2e

* tweak
2025-03-14 08:58:13 +05:30
Benny Joo 6319d63e1f fix: client-export warning for enterprise page (#20077) 2025-03-13 21:48:40 +00:00
Calcom BotandLingo.dev 2e18478b5c feat: update translations via @lingodotdev (#20074)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-13 20:08:24 +00:00
GitHub Actions 0ef93edd56 chore: release v5.0.19
Post release / release (push) Canceled after 0s
2025-03-13 19:08:48 +00:00
Eunjae LeeandBenny Joo d6a3661e58 fix: support more className-related props at DataTable (#19953)
* fix: support more className-related props at DataTable

* update style

* fix type error

* use relative import

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-03-13 14:25:57 -04:00
Alex van Andel be404436d7 fix: Do not self import @calcom/ui (#20050)
* fix: Do not self import @calcom/ui

* Make translations optional

* Fix mocking implementation of Button (never worked)

* Ensure other libraries can resolve AppListCard
2025-03-13 18:17:42 +00:00
Somay Chauhan b1ebefb854 feat: don't update query params for booker atom (#20000)
* feat: Add `omitUpdatingParams` prop to Booker store for query param control

* remove console

* Update booking.tsx

* fix: type check failing

* undo changes

* feat: Expose `onBookerStateChange` callback to track `booker-store` updates

* add `onBookerStateChange` docs

* Update store.ts

* added `allowUpdatingUrlParams` for backwards compatibility
2025-03-13 23:47:24 +05:30
f8aa2701b6 fix: add booking uid and link to insights routing downloads (#20007)
* fix: add booking uid and link to insights routing downloads

* minor change

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-03-13 14:11:02 -04:00
Keith Williams 034dfc8e6b perf: Ignore node_modules in Tailwind preset (#20058) 2025-03-13 18:05:58 +00:00
sean-brydon a6b6898697 add conditional (#20057) 2025-03-13 15:58:19 +00:00
Udit Takkar 0e5a0bfa7b feat: store cal video recording on s3 (#17050)
* feat: store cal video recording on s3

* fix: type error

* chore: update name and add condition
2025-03-13 12:46:10 -03:00
Eunjae Lee 6c01f5eace fix: reset page on filter change and remove client-side filtering (#20043)
* fix: reset page on filter change

* remove unused filterFn
2025-03-13 15:25:10 +00:00
VachmaraandTushar Bhatt ef2c34c55f fix: Intercom app - regex valid url (#20033)
* fix: regex valid url

* fix: only https urls

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-13 13:46:23 +00:00
Udit Takkar 7c1582c06f fix: circular dependency in handleNewBooking (#19890)
* fix: circular dependency

* fix: get orginial scheduled booking

* fix: create logger

* fix: type err
2025-03-13 13:45:00 +00:00
367e2666c6 fix: apply standard pagination to /bookings (#19973)
* fix: apply standard pagination to /bookings

* rename params

* fix type error

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-03-13 12:32:07 +00:00
Hariom Balhara 209a401113 fix: Enable feature for embeds when 100 pc is set (#20003)
* Enable feature for embeds when 100 pc is set

* Dont consider reserved status for quick availability check
2025-03-13 17:16:43 +05:30
837349e3d3 feat: Organization onboarding improvement - Payment before creation of org but onboarding doesn't require payment (#18990)
* create seed for org upgrade

* migrate about/new pages

* pages refactor + use persistant zustand store

* invited members view + confirm button

* add check to org slug

* check reserved subdomain

* remove quotes from subdomain env var

* intro to creation + billing

* create price

* fix types

* createWithPaymentIntent + permission check on teams

* open stripe link in popup

* intro to tests

* organization price and seat override tests

* move permissions to a new permission service

* update env and permission check

* intro to paid invoice trigger

* dont use subId

* wip

* Get e2e working with migration of teams and members

* fix ts errors

* Get flow working again

* Fix various issues and refactor

* Some more fixes

* Fix wrong page route link

* Platform onboarding fix and moving members of team to org rix

* Platform onboarding fix and moving members of team to org rix

* Fix tests, found a bug

* Get custom price flow working

* Get admin impersonation flow working for a non-existent user

* Fix unit test

* Admin onboarding handover

* fix admin onboarding tests

* fix ts error

* Get updateQuantity working

* More fixes

* fix test

* fix schema name

* Add tests

* Add missing file

* More tests

* Add more tests

* fix mt-2 moving down input into overflow

* fix handover layout removing HOC

* Update PR_TODO.md

* fix ts error due to merge conflict

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2025-03-13 11:17:43 +00:00
Hariom Balhara e66602aa5c Disable connected forms - Temporarily (#20040) 2025-03-13 11:14:23 +00:00
nizzyandUdit Takkar 082f8e1b04 fix: disable plain for self hosters (#19853)
* fix: disable plain for self hosters

* fix type error

* test update

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-03-13 08:50:16 +00:00
07716474d3 feat: Fetch previous ooo records using DateRangeFilter (#19596)
* teamMates OOO is visible for teamMember

* Update packages/features/settings/outOfOffice/CreateOrEditOutOfOfficeModal.tsx

Co-authored-by: Omar López <zomars@me.com>

* nit

* fixed tests, admin chk reqd for forMembers only

* check reqd in case of teamOOO only

* datefilter with default and improved search filter

* fix preset options

* fix ooo tests, due to default date filter

* ensure ooo test's wont fail when run on different dates of the month, due to default filter

* improvement: instead of 'Date to Year' safer is customRange

* added test for non-admin team ooo

* show disabled icons instead of notshowing

* tests for dateRangeFilters

* some more daterangefilter tests

* nit

* include only fetch prev records in this pr

* nit

* chore

* chore

* review comments

* chore

* update to fix tests

* nit

* nit

* update pagination mode

* removed unrelated changes

* remove unrelated changes

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2025-03-13 12:54:20 +05:30
Udit Takkar f335cc4269 fix: hide slot unavailable message for instant meetings (#20002)
* fix: hide slot unavailable message for instant meetings

* fix:
2025-03-13 07:22:02 +00:00
Benny Joo f19750cfaf chore: remove campsite from app store (#20032) 2025-03-12 23:23:19 -04:00
Benny Joo 46810f69ca fix: no-meeting-found is exporting client component from RSC (#20027) 2025-03-12 17:33:05 +00:00
amrit a7247c594b fix: round robin locale err (#20024)
* fix: round robin locale err

* Update bookings-single-view.tsx
2025-03-12 17:01:17 +00:00
Morgan 8de7876f72 chore: add CalendarService Exception Filter api v2 (#20021) 2025-03-12 16:24:50 +00:00
Morgan 49f75f5070 fix: lbraries/app-store missing tsconfig (#20006) 2025-03-12 14:55:18 +00:00
Lauris Skraucis 4ff543efc7 docs: v2 reschedule and cancel booking (#20011) 2025-03-12 16:36:32 +02:00
sean-brydon 8af5145b56 fixes extra span causing issues (#20004) 2025-03-12 12:12:13 +00:00
Lauris Skraucis a31242cdc5 refactor: v2 booking locations (#19613)
* refactor: v2 booking locations

* regenerate docs

* remove describe only

* update docs

* tests

* tests

* remove as any

* fix tests

* log

* remove unused dependency

* fix: bookings module dependency

* fix test

* fix: allow all integrations as booking location

* refactor: add organizersDefaultApp

* refactor: handle all integrations
2025-03-12 13:03:01 +01:00
Udit Takkar e20df1e7aa fix: remove Trans component (#20008) 2025-03-12 11:24:20 +00:00
sean-brydon 960a368175 fix sentry errors (#20005) 2025-03-12 10:53:17 +00:00
sean-brydonandTushar Bhatt 49cb86dfe9 fix: add improved logging and decrease ttl (#19979)
* add improved logging and decrease ttl

* fix test TTL time

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-12 10:26:07 +00:00
Udit Takkar b644ec58af fix: push notification page (#19999) 2025-03-12 10:09:55 +00:00
GitHub Actions bd92b89bf0 chore: release v5.0.18
Post release / release (push) Canceled after 0s
2025-03-12 07:42:40 +00:00
Benny Joo 495ac74255 fix: importing from @calcom/emails exposed daily-video api route to client code (#19994)
* create a new file

* use from new file

* add comment
2025-03-12 06:05:53 +00:00
Calcom BotandLingo.dev c3d35fd8c8 feat: update translations via @lingodotdev (#19992)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-12 04:48:31 +00:00
Benny Joo 87bc8d649a feat: add sentry report wrapper to every api route in appDir (#19966)
* update oauth tests

* fix parseRequestData

* add logging

* add fallback

* use ??

* updates

* use parseUrlFormData in saml/callback

* update tests

* fix

* remove log

* update

* any -> unknown

* use HttpErrors

* addressed

* unknown -> any

* feat: Add entry report wrapper to every api route in appDir

* fix

* fix unit test

* fix
2025-03-11 23:38:49 -04:00
785 changed files with 14169 additions and 5961 deletions
+10 -1
View File
@@ -17,6 +17,7 @@
#
# To enable enterprise-only features please add your environment variable to the .env file then make your way to /auth/setup to select your license and follow instructions.
CALCOM_LICENSE_KEY=
# Signature token for the Cal.com License API (used for self-hosted integrations)
# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key
@@ -192,7 +193,10 @@ NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
STRIPE_TEAM_MONTHLY_PRICE_ID=
STRIPE_TEAM_PRODUCT_ID=
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
STRIPE_ORG_MONTHLY_PRICE_ID=
STRIPE_ORG_PRODUCT_ID=
STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_SECRET_APPS=
STRIPE_PRIVATE_KEY=
@@ -285,7 +289,7 @@ NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL=
# use organizations
ORGANIZATIONS_ENABLED=
NEXT_PUBLIC_ORGANIZATIONS_MIN_SELF_SERVE_SEATS=30
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE=3700 # $37.00 per seat
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE_NEW=37 # $37.00 per seat
# This variable should only be set to 1 or true if you want to autolink external provider sign-ups with
# existing organizations based on email domain address
@@ -402,6 +406,11 @@ NEXT_PUBLIC_WEBSITE_TERMS_URL=
# [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal]
NEXT_PUBLIC_LOGGER_LEVEL=
# For storing Daily Video recordings on S3 Bucket
CAL_VIDEO_BUCKET_NAME=
CAL_VIDEO_BUCKET_REGION=
CAL_VIDEO_ASSUME_ROLE_ARN=
# Used to use Replexica SDK, a tool for real-time AI-powered localization
REPLEXICA_API_KEY=
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
View File
+6
View File
@@ -0,0 +1,6 @@
undecided:
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
-9
View File
@@ -1,9 +0,0 @@
- [x] Refactor individual calendar links functions to create the variables and just use the variables in the string literal instead of functions being called in the string literal
- [x] Must ensure during review that logically there is no change in the functionality of booking-single-view.tsx
- Testing
- [x] Verify Google Calendar, Ics, Office 365 - Compare with Production
- [x] Event Description, Booking title, Booking location, Start time, End time - All correct
- [x] Recurring event?
- [x] Dynamic Group booking?
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.147",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.152",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
+2
View File
@@ -21,6 +21,7 @@ import {
X_CAL_PLATFORM_EMBED,
} from "@calcom/platform-constants";
import { CalendarServiceExceptionFilter } from "./filters/calendar-service-exception.filter";
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
@@ -76,6 +77,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
app.useGlobalFilters(new ZodExceptionFilter());
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalFilters(new TRPCExceptionFilter());
app.useGlobalFilters(new CalendarServiceExceptionFilter());
app.use(cookieParser());
@@ -1,15 +1,20 @@
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { BillingModule } from "@/modules/billing/billing.module";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UsersModule } from "@/modules/users/users.module";
@@ -33,6 +38,11 @@ import { Module } from "@nestjs/common";
ApiKeysRepository,
OAuthClientUsersService,
PlatformBookingsService,
CalendarsService,
CredentialsRepository,
AppsRepository,
CalendarsRepository,
SelectedCalendarsRepository,
],
controllers: [BookingsController_2024_04_15],
})
@@ -4,19 +4,24 @@ import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bo
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { BillingModule } from "@/modules/billing/billing.module";
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UsersModule } from "@/modules/users/users.module";
@@ -47,6 +52,11 @@ import { Module } from "@nestjs/common";
BookingSeatRepository,
ApiKeysRepository,
PlatformBookingsService,
CalendarsService,
CredentialsRepository,
AppsRepository,
CalendarsRepository,
SelectedCalendarsRepository,
],
controllers: [BookingsController_2024_08_13],
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
@@ -39,11 +39,15 @@ import { Request } from "express";
import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import {
CancelBookingInput,
CancelBookingInput_2024_08_13,
CancelBookingInputPipe,
CancelSeatedBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
RescheduleBookingInput,
RescheduleBookingInput_2024_08_13,
RescheduleBookingInputPipe,
RescheduleSeatedBookingInput_2024_08_13,
} from "@calcom/platform-types";
import {
CreateBookingInputPipe,
@@ -65,9 +69,12 @@ import {
@DocsTags("Bookings")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-08-13\``,
description: `Must be set to ${VERSION_2024_08_13}`,
example: VERSION_2024_08_13,
required: true,
schema: {
default: VERSION_2024_08_13,
},
})
export class BookingsController_2024_08_13 {
private readonly logger = new Logger("BookingsController_2024_08_13");
@@ -99,7 +106,7 @@ export class BookingsController_2024_08_13 {
],
},
description:
"Accepts different types of booking input: CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, or CreateRecurringBookingInput_2024_08_13",
"Accepts different types of booking input: Create Booking (Option 1), Create Instant Booking (Option 2), or Create Recurring Booking (Option 3)",
})
@ApiExtraModels(
CreateBookingInput_2024_08_13,
@@ -172,9 +179,19 @@ export class BookingsController_2024_08_13 {
@UseGuards(BookingUidGuard)
@ApiOperation({
summary: "Reschedule a booking",
description:
"Reschedule a booking by passing `:bookingUid` of the booking that should be rescheduled and pass request body with a new start time to create a new booking.",
description: "Reschedule a booking or seated booking",
})
@ApiBody({
schema: {
oneOf: [
{ $ref: getSchemaPath(RescheduleBookingInput_2024_08_13) },
{ $ref: getSchemaPath(RescheduleSeatedBookingInput_2024_08_13) },
],
},
description:
"Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2)",
})
@ApiExtraModels(RescheduleBookingInput_2024_08_13, RescheduleSeatedBookingInput_2024_08_13)
async rescheduleBooking(
@Param("bookingUid") bookingUid: string,
@Body(new RescheduleBookingInputPipe())
@@ -199,6 +216,17 @@ export class BookingsController_2024_08_13 {
For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
})
@ApiBody({
schema: {
oneOf: [
{ $ref: getSchemaPath(CancelBookingInput_2024_08_13) },
{ $ref: getSchemaPath(CancelSeatedBookingInput_2024_08_13) },
],
},
description:
"Accepts different types of cancel booking input: Cancel Booking (Option 1) or Cancel Seated Booking (Option 2)",
})
@ApiExtraModels(CancelBookingInput_2024_08_13, CancelSeatedBookingInput_2024_08_13)
async cancelBooking(
@Req() request: Request,
@Param("bookingUid") bookingUid: string,
@@ -40,6 +40,8 @@ import {
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
describe("Seated bookings", () => {
let app: INestApplication;
let organization: Team;
@@ -330,7 +332,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
location: googleMeetUrl,
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
@@ -383,6 +385,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: body.attendee.name,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -418,6 +424,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: body.attendee.name,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -797,7 +807,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
location: googleMeetUrl,
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
@@ -850,6 +860,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: body.attendee.name,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -885,6 +899,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: body.attendee.name,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -910,7 +928,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
location: googleMeetUrl,
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
@@ -966,6 +984,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: nameAttendeeOne,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
});
@@ -982,6 +1004,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: nameAttendeeTwo,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -1017,6 +1043,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: nameAttendeeOne,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
});
@@ -1030,6 +1060,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: nameAttendeeTwo,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -1,6 +1,7 @@
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 { 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";
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";
@@ -31,6 +32,7 @@ import {
RecurringBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
@@ -66,6 +68,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
const team2EventTypeSlug = `team-bookings-event-type-${randomString()}`;
const phoneOnlyEventTypeSlug = `team-bookings-event-type-${randomString()}`;
let phoneBasedBooking: BookingOutput_2024_08_13;
beforeAll(async () => {
const moduleRef = await withApiAuth(
teamUserEmail,
@@ -408,12 +412,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
it("should create a phone based booking", async () => {
const phoneNumber = "+919876543210";
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
eventTypeId: phoneOnlyEventTypeId,
attendee: {
name: "alice",
phoneNumber: "+919876543210",
phoneNumber,
timeZone: "Europe/Madrid",
language: "es",
},
@@ -453,6 +458,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
expect(data.meetingUrl).toEqual(body.meetingUrl);
expect(data.absentHost).toEqual(false);
expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual(phoneNumber);
phoneBasedBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
@@ -619,6 +626,59 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("reschedule", () => {
it("should should reschedule phone based booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${phoneBasedBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts.length).toEqual(1);
expect(data.hosts[0].id).toEqual(teamUser.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 14, 15, 0)).toISOString());
expect(data.duration).toEqual(15);
expect(data.eventTypeId).toEqual(phoneOnlyEventTypeId);
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: phoneBasedBooking.attendees[0].name,
email: phoneBasedBooking.attendees[0].email,
phoneNumber: phoneBasedBooking.attendees[0].phoneNumber,
timeZone: phoneBasedBooking.attendees[0].timeZone,
language: phoneBasedBooking.attendees[0].language,
absent: false,
});
expect(data.meetingUrl).toEqual(phoneBasedBooking.meetingUrl);
expect(data.absentHost).toEqual(false);
expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual(
phoneBasedBooking.bookingFieldsResponses.attendeePhoneNumber
);
phoneBasedBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
@@ -4,6 +4,7 @@ import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.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 { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.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";
@@ -27,10 +28,12 @@ import { withApiAuth } from "test/utils/withApiAuth";
import {
CAL_API_VERSION_HEADER,
SUCCESS_STATUS,
VERSION_2024_06_14,
VERSION_2024_08_13,
X_CAL_CLIENT_ID,
} from "@calcom/platform-constants";
import {
CreateEventTypeInput_2024_06_14,
GetBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
@@ -95,6 +98,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
organization = await teamRepositoryFixture.create({
name: `user-bookings-organization-${randomString()}`,
isOrganization: true,
});
oAuthClient = await createOAuthClient(organization.id);
@@ -146,7 +150,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
startTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: "booking-in-the-past",
uid: `booking-in-the-past-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
@@ -326,6 +330,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
it("should create a booking", async () => {
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId,
@@ -335,7 +340,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
location: googleMeetUrl,
bookingFieldsResponses: {
customField: "customValue",
},
@@ -389,6 +394,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
email: body.attendee.email,
...body.bookingFieldsResponses,
guests: body.guests,
location: {
optionValue: googleMeetUrl,
value: "link",
},
});
expect(data.guests).toEqual(body.guests);
@@ -972,6 +981,64 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("createdAt filter", () => {
it("should should get bookings after specified createdAt time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?afterCreatedAt=${createdRecurringBooking[1].createdAt}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(2);
});
});
it("should should get bookings before specified createdAt time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?beforeCreatedAt=${createdRecurringBooking[0].createdAt}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(3);
});
});
it("should should get bookings in createdAt range", async () => {
return request(app.getHttpServer())
.get(
`/v2/bookings?afterCreatedAt=${createdRecurringBooking[0].createdAt}&beforeCreatedAt=${createdRecurringBooking[1].createdAt}`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(2);
});
});
});
it("should should sort bookings by start in descending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=desc`)
@@ -1537,6 +1604,440 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("booking location", () => {
const address = "123 Main St";
const link = "https://cal.com/join/123456";
const phone = "+37121999999";
let eventTypeWithAllLocationsId: number;
it("can create event type with all locations except google meet", async () => {
const eventTypeBody: CreateEventTypeInput_2024_06_14 = {
title: "book using any location",
slug: "book-using-any-location",
lengthInMinutes: 15,
locations: [
{
type: "integration",
integration: "cal-video",
},
{
type: "address",
address,
public: true,
},
{
type: "link",
link,
public: true,
},
{
type: "phone",
phone,
public: true,
},
{
type: "attendeeAddress",
},
{
type: "attendeePhone",
},
{
type: "attendeeDefined",
},
],
};
const eventTypeResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(eventTypeBody)
.expect(201);
const eventTypeResponseBody: CreateEventTypeOutput_2024_06_14 = eventTypeResponse.body;
const createdEventType = eventTypeResponseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.locations).toHaveLength(7);
expect(createdEventType.locations).toEqual(createdEventType.locations);
eventTypeWithAllLocationsId = createdEventType.id;
});
it("can book with cal video location", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "integration",
integration: "cal-video",
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(
createdBooking.location?.startsWith("http") || createdBooking.location === "integrations:daily"
).toEqual(true);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with address location", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "address",
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(address);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with link location", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "link",
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(link);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with link location", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "phone",
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(phone);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with attendeeAddress location", async () => {
const attendeeAddress = "123 Example St, City, Valhalla";
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "attendeeAddress",
address: attendeeAddress,
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(attendeeAddress);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
xit("can book with attendeeAddress location", async () => {
const attendeePhone = "+37120993151";
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "attendeePhone",
phone: attendeePhone,
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(attendeePhone);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with attendeeDefined location", async () => {
const attendeeDefinedLocation = "namek 100";
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "attendeeDefined",
location: attendeeDefinedLocation,
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(attendeeDefinedLocation);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can't book with not location that is not in event type", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "integration",
integration: "google-meet",
},
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
it("can't book with invalid location type", async () => {
const bookingBody = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "blablabala",
},
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
});
describe("rescheduling booking with invalid booking responses", () => {
it("should reschedule", async () => {
const email = `user-bookings-attendee-${randomString(10)}@gmail.com`;
const username = `user-bookings-attendee-${randomString(10)}`;
const locale = "it";
const timeZone = "Europe/Rome";
const attendee = await userRepositoryFixture.create({
email,
username,
locale,
timeZone,
});
const booking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${randomString(10)}`,
title: "booking title",
startTime: "2060-09-05T11:00:00.000Z",
endTime: "2060-09-05T12:00:00.000Z",
eventType: {
connect: {
id: eventTypeId,
},
},
status: "ACCEPTED",
metadata: {},
responses: "null",
user: {
connect: {
id: user.id,
},
},
attendees: {
create: {
email: email,
name: username,
timeZone,
locale,
},
},
});
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2060, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${booking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2060, 0, 9, 15, 0, 0)).toISOString());
expect(data.rescheduledFromUid).toEqual(booking.uid);
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual("accepted");
expect(data.eventTypeId).toEqual(booking.eventTypeId);
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: attendee.username,
email: attendee.email,
timeZone: attendee.timeZone,
language: attendee.locale,
absent: false,
});
expect(data.bookingFieldsResponses).toEqual({
name: attendee.username,
email: attendee.email,
rescheduledReason: body.reschedulingReason,
guests: [],
});
await bookingsRepositoryFixture.deleteById(data.id);
await userRepositoryFixture.delete(attendee.id);
});
});
});
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
return (
!Array.isArray(data) &&
@@ -229,8 +229,7 @@ export class BookingsService_2024_08_13 {
const fetchedBookings: { bookings: { id: number }[] } = await getAllUserBookings({
bookingListingByStatus: queryParams.status || [],
skip: queryParams.skip ?? 0,
// note(Lauris): we substract -1 because getAllUSerBookings child function adds +1 for some reason
take: queryParams.take ? queryParams.take - 1 : 100,
take: queryParams.take ?? 100,
filters: this.inputService.transformGetBookingsFilters(queryParams),
ctx: {
user,
@@ -564,7 +563,9 @@ export class BookingsService_2024_08_13 {
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
}
const eventType = await this.eventTypesRepository.getEventTypeByIdIncludeUsersAndTeam(booking.eventTypeId);
const eventType = await this.eventTypesRepository.getEventTypeByIdIncludeUsersAndTeam(
booking.eventTypeId
);
if (!eventType) {
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
}
@@ -5,7 +5,10 @@ import {
} from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { defaultBookingResponses } from "@/lib/safe-parse/default-responses-booking";
import { safeParse } from "@/lib/safe-parse/safe-parse";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
@@ -15,13 +18,17 @@ import { BadRequestException, Injectable, NotFoundException } from "@nestjs/comm
import { Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { CreationSource } from "@prisma/client";
import { isURL, isPhoneNumber } from "class-validator";
import { Request } from "express";
import { DateTime } from "luxon";
import { NextApiRequest } from "next/types";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
import {
EventTypeMetaDataSchema,
apiToInternalintegrationsMapping,
} from "@calcom/platform-libraries/event-types";
import {
CancelBookingInput,
CancelBookingInput_2024_08_13,
@@ -35,6 +42,7 @@ import {
RescheduleBookingInput_2024_08_13,
RescheduleSeatedBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { BookingInputLocation_2024_08_13 } from "@calcom/platform-types/bookings/2024-08-13/inputs/location.input";
import { EventType } from "@calcom/prisma/client";
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
@@ -77,6 +85,7 @@ export class InputBookingsService_2024_08_13 {
private readonly config: ConfigService,
private readonly apiKeyRepository: ApiKeysRepository,
private readonly bookingSeatRepository: BookingSeatRepository,
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
private readonly platformBookingsService: PlatformBookingsService,
private readonly usersRepository: UsersRepository
) {}
@@ -91,23 +100,21 @@ export class InputBookingsService_2024_08_13 {
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const location = request.body.location || request.body.meetingUrl;
this.logger.log(`createBookingRequest_2024_04_15`, {
this.logger.log(`createBookingRequest_2024_08_13`, {
requestId: request.get("X-Request-Id"),
ownerId: userId,
location,
oAuthClientParams,
});
if (oAuthClientParams) {
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
Object.assign(newRequest, { userId, ...oAuthClientParams });
newRequest.body = {
...bodyTransformed,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
Object.assign(newRequest, { userId });
newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 };
}
@@ -143,6 +150,10 @@ export class InputBookingsService_2024_08_13 {
)
: inputBooking.attendee.email;
const inputLocation = inputBooking.location || inputBooking.meetingUrl;
this.isBookingLocationWithEventTypeLocations(inputLocation, eventType);
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
return {
start: startTime.toISO(),
end: endTime.toISO(),
@@ -159,6 +170,7 @@ export class InputBookingsService_2024_08_13 {
email: attendeeEmail ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
location,
},
};
}
@@ -196,17 +208,14 @@ export class InputBookingsService_2024_08_13 {
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const location = request.body.location || request.body.meetingUrl;
if (oAuthClientParams) {
Object.assign(newRequest, {
userId,
...oAuthClientParams,
platformBookingLocation: location,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
});
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
Object.assign(newRequest, { userId });
}
newRequest.body = bodyTransformed.map((event) => ({
@@ -217,6 +226,144 @@ export class InputBookingsService_2024_08_13 {
return newRequest as unknown as BookingRequest;
}
transformLocation(location: string | BookingInputLocation_2024_08_13): {
value: string;
optionValue: string;
} {
if (typeof location === "string") {
// note(Lauris): this is for backwards compatibility because before switching to booking location objects
// we only received a string. If someone is complaining that their location is not displaying as a URL
// or whatever check that they are not providing a string for bookign location but one of the input objects.
if (isURL(location, { require_protocol: false }) || location.startsWith("www.")) {
return {
value: "link",
optionValue: location,
};
}
if (isPhoneNumber(location)) {
return {
value: "phone",
optionValue: location,
};
}
return {
value: "somewhereElse",
optionValue: location,
};
}
if (location.type === "integration") {
const integration = apiToInternalintegrationsMapping[location.integration];
if (!integration) {
throw new BadRequestException(`Invalid integration: ${location.integration}`);
}
return {
value: integration,
optionValue: "",
};
}
if (location.type === "address") {
return {
value: "inPerson",
optionValue: "",
};
}
if (location.type === "attendeeAddress") {
return {
value: "attendeeInPerson",
optionValue: location.address,
};
}
if (location.type === "link") {
return {
value: "link",
optionValue: "",
};
}
if (location.type === "phone") {
return {
value: "userPhone",
optionValue: "",
};
}
if (location.type === "organizersDefaultApp") {
return {
value: "conferencing",
optionValue: "",
};
}
if (location.type === "attendeePhone") {
return {
value: "phone",
optionValue: location.phone,
};
}
if (location.type === "attendeeDefined") {
return {
value: "somewhereElse",
optionValue: location.location,
};
}
throw new BadRequestException(
`Booking location with type ${(location as BookingInputLocation_2024_08_13).type} not valid.`
);
}
private isBookingLocationWithEventTypeLocations(
inputBookingLocation: undefined | string | BookingInputLocation_2024_08_13,
dbEventType: EventType
) {
if (!inputBookingLocation || typeof inputBookingLocation === "string") {
// note(Lauris): for backwards compatibility because we had string locations before so let them pass.
return true;
}
const eventTypeLocations = this.outputEventTypesService.transformLocations(dbEventType.locations);
const allowedLocationTypes = eventTypeLocations.map((location) => location.type);
const isAllowed = allowedLocationTypes.includes(inputBookingLocation.type);
if (!isAllowed) {
throw new BadRequestException(
`Booking location with type ${inputBookingLocation.type} not valid for event type with id=${
dbEventType.id
}. The event type has following location types: ${allowedLocationTypes.join(
", "
)}, and only these types are allowed for booking location.`
);
}
if (inputBookingLocation.type === "integration" && "integration" in inputBookingLocation) {
const allowedIntegrations = eventTypeLocations
.filter((location) => location.type === "integration")
.map((location) => location.integration);
const isAllowedIntegration = allowedIntegrations.includes(inputBookingLocation.integration);
if (!isAllowedIntegration) {
throw new BadRequestException(
`Booking location with integration ${
inputBookingLocation.integration
} not valid for event type with id=${
dbEventType.id
}. The event type has following integrations: ${allowedIntegrations.join(
", "
)}, and only these integrations are allowed for booking location.`
);
}
}
return true;
}
async transformInputCreateRecurringBooking(
inputBooking: CreateRecurringBookingInput_2024_08_13,
platformClientId?: string
@@ -262,6 +409,10 @@ export class InputBookingsService_2024_08_13 {
)
: inputBooking.attendee.email;
const inputLocation = inputBooking.location || inputBooking.meetingUrl;
this.isBookingLocationWithEventTypeLocations(inputLocation, eventType);
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
for (let i = 0; i < repeatsTimes; i++) {
const endTime = startTime.plus({ minutes: eventType.length });
@@ -280,7 +431,9 @@ export class InputBookingsService_2024_08_13 {
...(inputBooking.bookingFieldsResponses || {}),
name: inputBooking.attendee.name,
email: attendeeEmail,
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
location,
},
schedulingType: eventType.schedulingType,
});
@@ -417,8 +570,19 @@ export class InputBookingsService_2024_08_13 {
throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`);
}
const bookingResponses = bookingResponsesSchema.parse(booking.responses);
const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
const bookingResponses = safeParse(
bookingResponsesSchema,
booking.responses,
defaultBookingResponses,
false
);
const bookingResponsesMissing =
bookingResponses.name === defaultBookingResponses.name &&
bookingResponses.email === defaultBookingResponses.email;
const attendee = bookingResponsesMissing
? booking.attendees[0]
: booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
if (!attendee) {
throw new NotFoundException(
@@ -426,6 +590,12 @@ export class InputBookingsService_2024_08_13 {
);
}
if (bookingResponsesMissing) {
bookingResponses.name = attendee.name;
bookingResponses.email = attendee.email;
bookingResponses.attendeePhoneNumber = attendee.phoneNumber || undefined;
}
const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone);
const endTime = startTime.plus({ minutes: eventType.length });
@@ -482,6 +652,8 @@ export class InputBookingsService_2024_08_13 {
queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined),
afterUpdatedDate: queryParams.afterUpdatedAt,
beforeUpdatedDate: queryParams.beforeUpdatedAt,
afterCreatedDate: queryParams.afterCreatedAt,
beforeCreatedDate: queryParams.beforeCreatedAt,
};
}
@@ -27,6 +27,7 @@ import { Booking, BookingSeat } from "@calcom/prisma/client";
export const bookingResponsesSchema = z
.object({
email: z.string(),
attendeePhoneNumber: z.string().optional(),
name: z.union([
z.string(),
z.object({
@@ -45,6 +46,7 @@ export const seatedBookingDataSchema = z
responses: z
.object({
email: z.string(),
attendeePhoneNumber: z.string().optional(),
name: z.union([
z.string(),
z.object({
@@ -116,6 +116,7 @@ describe("Event types Endpoints", () => {
slug: "email",
required: true,
disableOnPrefill: false,
hidden: false,
};
const defaultResponseBookingFieldLocation = {
@@ -336,6 +337,46 @@ describe("Event types Endpoints", () => {
.expect(404);
});
it("should not be able to create phone-only event type", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Phone coding consultation",
slug: "phone-coding-consultation",
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
bookingFields: [
{
type: "email",
required: false,
label: "Email",
hidden: true,
},
{
type: "phone",
slug: "attendeePhoneNumber",
required: true,
label: "Phone number",
hidden: false,
},
],
};
const response = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
expect(response.body.error.message).toBe(
"checkIsEmailUserAccessible - Email booking field must be required and visible"
);
});
it("should create an event type", async () => {
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
type: "name",
@@ -1191,7 +1232,14 @@ describe("Event types Endpoints", () => {
const expectedReturnSystemFields = [
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{
isDefault: true,
required: true,
slug: "email",
type: "email",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "radioInput",
@@ -1644,6 +1692,7 @@ describe("Event types Endpoints", () => {
slug: "email",
required: true,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
@@ -29,7 +29,12 @@ import {
} from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import {
EVENT_TYPE_READ,
EVENT_TYPE_WRITE,
SUCCESS_STATUS,
VERSION_2024_06_14,
} from "@calcom/platform-constants";
import {
UpdateEventTypeInput_2024_06_14,
GetEventTypesQuery_2024_06_14,
@@ -45,8 +50,12 @@ import {
@DocsTags("Event Types")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-06-14\``,
description: `Must be set to ${VERSION_2024_06_14}`,
example: VERSION_2024_06_14,
required: true,
schema: {
default: VERSION_2024_06_14,
},
})
export class EventTypesController_2024_06_14 {
constructor(
@@ -14,6 +14,8 @@ import {
updateEventType,
getEventTypesPublic,
EventTypesPublic,
SystemField,
CustomField,
} from "@calcom/platform-libraries/event-types";
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
import { EventType } from "@calcom/prisma/client";
@@ -31,6 +33,9 @@ export class EventTypesService_2024_06_14 {
) {}
async createUserEventType(user: UserWithProfile, body: InputEventTransformed_2024_06_14) {
if (body.bookingFields) {
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
}
await this.checkCanCreateEventType(user.id, body);
const eventTypeUser = await this.getUserToCreateEvent(user);
const { destinationCalendar: _destinationCalendar, ...rest } = body;
@@ -78,6 +83,16 @@ export class EventTypesService_2024_06_14 {
await this.checkUserOwnsSchedule(userId, body.scheduleId);
}
checkHasUserAccessibleEmailBookingField(bookingFields: (SystemField | CustomField)[]) {
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
if (!isEmailFieldRequiredAndVisible) {
throw new BadRequestException(
"checkIsEmailUserAccessible - Email booking field must be required and visible"
);
}
}
async getEventTypeByUsernameAndSlug(
username: string,
eventTypeSlug: string,
@@ -219,6 +234,9 @@ export class EventTypesService_2024_06_14 {
body: Partial<InputEventTransformed_2024_06_14>,
user: UserWithProfile
) {
if (body.bookingFields) {
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
}
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
@@ -37,8 +37,6 @@ import {
} from "@calcom/platform-types";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
interface ValidationContext {
eventTypeId?: number;
seatsPerTimeSlot?: number | null;
@@ -51,7 +49,6 @@ interface ValidationContext {
export class InputEventTypesService_2024_06_14 {
constructor(
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
private readonly calendarsService: CalendarsService
) {}
@@ -128,12 +125,11 @@ export class InputEventTypesService_2024_06_14 {
} = inputEventType;
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
const hasMultipleLocations = (locations || defaultLocations).length > 1;
const eventType = {
...rest,
length: lengthInMinutes,
locations: this.transformInputLocations(locations || defaultLocations),
bookingFields: this.transformInputBookingFields(bookingFields, hasMultipleLocations),
bookingFields: this.transformInputBookingFields(bookingFields),
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
@@ -182,15 +178,12 @@ export class InputEventTypesService_2024_06_14 {
: {};
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
const hasMultipleLocations = !!(locations && locations?.length > 1);
const eventType = {
...rest,
length: lengthInMinutes,
locations: locations ? this.transformInputLocations(locations) : undefined,
bookingFields: bookingFields
? this.transformInputBookingFields(bookingFields, hasMultipleLocations)
: undefined,
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
@@ -220,10 +213,7 @@ export class InputEventTypesService_2024_06_14 {
return transformLocationsApiToInternal(inputLocations);
}
transformInputBookingFields(
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
hasMultipleLocations: boolean
) {
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
const internalFields: (SystemField | CustomField)[] = inputBookingFields
? transformBookingFieldsApiToInternal(inputBookingFields)
: [];
@@ -252,7 +242,28 @@ export class InputEventTypesService_2024_06_14 {
systemCustomRescheduleReasonField || systemAfterFieldRescheduleReason,
];
return [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
const bookingFields = [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
if (!this.hasEmailOrPhoneOnlySetup(bookingFields)) {
throw new BadRequestException(
"Booking fields validation failed: visible and required email or visible and required attendee phone field is needed."
);
}
return bookingFields;
}
hasEmailOrPhoneOnlySetup(bookingFields: (SystemField | CustomField)[]) {
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
const attendeePhoneNumberField = bookingFields.find(
(field) => field.type === "phone" && field.name === "attendeePhoneNumber"
);
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
const isAttendeePhoneNumberFieldRequiredAndVisible =
attendeePhoneNumberField?.required && !attendeePhoneNumberField?.hidden;
return isEmailFieldRequiredAndVisible || isAttendeePhoneNumberFieldRequiredAndVisible;
}
isUserCustomField(field: SystemField | CustomField): field is CustomField {
@@ -39,8 +39,12 @@ import {
@DocsTags("Schedules")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-06-11\``,
description: `Must be set to ${VERSION_2024_06_11}`,
example: VERSION_2024_06_11,
required: true,
schema: {
default: VERSION_2024_06_11,
},
})
@ApiHeader({
name: "Authorization",
@@ -0,0 +1,57 @@
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
import { Request } from "express";
import { ERROR_STATUS } from "@calcom/platform-constants";
import {
CalendarAppError,
CalendarAppDelegationCredentialInvalidGrantError,
CalendarAppDelegationCredentialError,
CalendarAppDelegationCredentialConfigurationError,
CalendarAppDelegationCredentialClientIdNotAuthorizedError,
CalendarAppDelegationCredentialNotSetupError,
} from "@calcom/platform-libraries/app-store";
import { Response } from "@calcom/platform-types";
type CalendarError =
| CalendarAppError
| CalendarAppDelegationCredentialInvalidGrantError
| CalendarAppDelegationCredentialError
| CalendarAppDelegationCredentialConfigurationError
| CalendarAppDelegationCredentialNotSetupError
| CalendarAppDelegationCredentialClientIdNotAuthorizedError;
@Catch(
CalendarAppError,
CalendarAppDelegationCredentialInvalidGrantError,
CalendarAppDelegationCredentialError,
CalendarAppDelegationCredentialConfigurationError,
CalendarAppDelegationCredentialClientIdNotAuthorizedError,
CalendarAppDelegationCredentialNotSetupError
)
export class CalendarServiceExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger("CalendarServiceExceptionFilter");
catch(exception: CalendarError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const requestId = request.headers["X-Request-Id"];
this.logger.error(`Calendar Service Exception Filter: ${exception?.message}`, {
exception,
body: request.body,
headers: filterReqHeaders(request.headers),
url: request.url,
method: request.method,
requestId,
});
response.status(400).json({
status: ERROR_STATUS,
timestamp: new Date().toISOString(),
path: request.url,
error: { code: exception.name, message: exception.message },
});
}
}
+10 -9
View File
@@ -2,19 +2,20 @@ import { IncomingHttpHeaders } from "http";
export function filterReqHeaders(headers: IncomingHttpHeaders): Partial<IncomingHttpHeaders> {
return {
"Content-Type": headers["content-type"],
"User-Agent": headers["user-agent"],
"Content-Type": headers["Content-Type"] ?? headers["content-type"],
Authorization: headers["Authorization"] ?? headers["authorization"],
"X-Cal-Client-Id": headers["X-Cal-Client-Id"] ?? headers["x-cal-client-id"],
"Cal-Api-Version": headers["Cal-Api-Version"] ?? headers["cal-api-version"],
"X-Request-Id": headers["X-Request-Id"] ?? headers["x-request-id"],
// IP-related headers
"User-Agent": headers["User-agent"] ?? headers["user-agent"],
"X-Forwarded-For": headers["x-forwarded-for"],
"X-Forwarded-Host": headers["x-forwarded-host"],
"CF-Connecting-IP": headers["cf-connecting-ip"],
"CloudFront-Viewer-Address": headers["cloudfront-viewer-address"],
// Vercel headers
"x-vercel-id": headers["x-vercel-id"],
"x-vercel-deployment-url": headers["x-vercel-deployment-url"],
"x-vercel-country": headers["x-vercel-country"],
"x-vercel-region": headers["x-vercel-region"],
Host: headers["host"],
"X-Vercel-Id": headers["x-vercel-id"],
"X-Vercel-Deployment-Url": headers["x-vercel-deployment-url"],
"X-Vercel-Country": headers["x-vercel-country"],
"X-Vercel-Region": headers["x-vercel-region"],
Host: headers["Host"] ?? headers["host"],
};
}
@@ -2,7 +2,7 @@ export const defaultBookingResponses = {
name: "unknown",
email: "unknown",
guests: [],
rescheduleReason: "unknown",
rescheduledReason: "unknown",
};
export const defaultBookingMetadata = { videoCallUrl: "unknown" };
@@ -21,13 +21,23 @@ export class ResponseInterceptor implements NestInterceptor {
const { statusCode } = response;
const responseTime = Date.now() - startTime;
let jsonBodyString = "{}";
try {
if (data && typeof data === "object") {
jsonBodyString = JSON.stringify(data);
}
} catch (err) {
this.logger.error("Could not parse request body");
}
this.logger.log("Outgoing Response", {
requestId,
method,
url,
statusCode,
responseTime,
responseBody: data,
responseBody: jsonBodyString,
timestamp: new Date().toISOString(),
});
})
@@ -11,13 +11,22 @@ export class RequestIdMiddleware implements NestMiddleware {
const requestId = uuid();
req.headers["X-Request-Id"] = requestId;
const { method, headers, body: requestBody, baseUrl } = req;
let jsonBodyString = "{}";
try {
if (requestBody && typeof requestBody === "object") {
jsonBodyString = JSON.stringify(requestBody);
}
} catch (err) {
this.logger.error("Could not parse request body");
}
this.logger.log("Incoming Request", {
requestId,
method,
url: baseUrl,
headers: filterReqHeaders(headers),
requestBody,
requestBody: jsonBodyString,
timestamp: new Date().toISOString(),
});
@@ -35,12 +35,13 @@ import {
Redirect,
UnauthorizedException,
Req,
HttpException,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToInstance } from "class-transformer";
import { Request } from "express";
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS, OFFICE_365_VIDEO } from "@calcom/platform-constants";
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS, OFFICE_365_VIDEO, CAL_VIDEO } from "@calcom/platform-constants";
export type OAuthCallbackState = {
accessToken: string;
@@ -56,7 +57,7 @@ export type OAuthCallbackState = {
})
@DocsTags("Conferencing")
export class ConferencingController {
private readonly logger = new Logger("Platform Gcal Provider");
private readonly logger = new Logger("ConferencingController");
constructor(
private readonly tokensRepository: TokensRepository,
@@ -70,6 +71,12 @@ export class ConferencingController {
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Connect your conferencing application" })
@ApiParam({
name: "app",
description: "Conferencing application type",
enum: [GOOGLE_MEET],
required: true,
})
async connect(
@GetUser("id") userId: number,
@Param("app") app: string
@@ -92,6 +99,12 @@ export class ConferencingController {
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Get OAuth conferencing app auth url" })
@ApiParam({
name: "app",
description: "Conferencing application type",
enum: [ZOOM, OFFICE_365_VIDEO],
required: true,
})
async redirect(
@Req() req: Request,
@Headers("Authorization") authorization: string,
@@ -137,6 +150,12 @@ export class ConferencingController {
@UseGuards()
@Redirect(undefined, 301)
@ApiOperation({ summary: "conferencing apps oauths callback" })
@ApiParam({
name: "app",
description: "Conferencing application type",
enum: [ZOOM, OFFICE_365_VIDEO],
required: true,
})
async save(
@Query("state") state: string,
@Param("app") app: string,
@@ -144,6 +163,10 @@ export class ConferencingController {
@Query("error") error: string | undefined,
@Query("error_description") error_description: string | undefined
): Promise<{ url: string }> {
if (!state) {
throw new BadRequestException("Missing `state` query param");
}
const decodedCallbackState: OAuthCallbackState = JSON.parse(state);
try {
const userId = await this.tokensRepository.getAccessTokenOwnerId(decodedCallbackState.accessToken);
@@ -169,6 +192,9 @@ export class ConferencingController {
);
}
} catch (error) {
if (error instanceof HttpException || error instanceof Error) {
this.logger.error(error.message);
}
return {
url: decodedCallbackState.onErrorReturnTo ?? "",
};
@@ -195,6 +221,12 @@ export class ConferencingController {
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Set your default conferencing application" })
@ApiParam({
name: "app",
description: "Conferencing application type",
enum: [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO, CAL_VIDEO],
required: true,
})
async default(
@GetUser() user: UserWithProfile,
@Param("app") app: string
@@ -216,6 +248,12 @@ export class ConferencingController {
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Disconnect your conferencing application" })
@ApiParam({
name: "app",
description: "Conferencing application type",
enum: [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO],
required: true,
})
async disconnect(
@GetUser() user: UserWithProfile,
@Param("app") app: string
@@ -26,6 +26,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { randomString } from "test/utils/randomString";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { slugify } from "@calcom/platform-libraries";
import { ApiSuccessResponse } from "@calcom/platform-types";
const CLIENT_REDIRECT_URI = "http://localhost:4321";
@@ -228,6 +229,9 @@ describe("OAuth Client Users Endpoints", () => {
expect(responseBody.data.user.timeFormat).toEqual(requestBody.timeFormat);
expect(responseBody.data.user.locale).toEqual(requestBody.locale);
expect(responseBody.data.user.avatarUrl).toEqual(requestBody.avatarUrl);
const [emailUser, emailDomain] = responseBody.data.user.email.split("@");
const [domainName, TLD] = emailDomain.split(".");
expect(responseBody.data.user.username).toEqual(slugify(`${emailUser}-${domainName}-${TLD}`));
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
@@ -546,6 +550,9 @@ describe("OAuth Client Users Endpoints", () => {
expect(responseBody.data.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, userUpdatedEmail)
);
const [emailUser, emailDomain] = responseBody.data.email.split("@");
const [domainName, TLD] = emailDomain.split(".");
expect(responseBody.data.username).toEqual(slugify(`${emailUser}-${domainName}-${TLD}`));
expect(responseBody.data.locale).toEqual(Locales.PT_BR);
});
@@ -29,19 +29,12 @@ import {
NotFoundException,
BadRequestException,
} from "@nestjs/common";
import {
ApiOperation as DocsOperation,
ApiCreatedResponse as DocsCreatedResponse,
ApiTags,
} from "@nestjs/swagger";
import { ApiCreatedResponse as DocsCreatedResponse, ApiTags } from "@nestjs/swagger";
import { User, MembershipRole } from "@prisma/client";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { CreateOAuthClientInput, UpdateOAuthClientInput, Pagination } from "@calcom/platform-types";
const AUTH_DOCUMENTATION = `⚠️ First, this endpoint requires \`Cookie: next-auth.session-token=eyJhbGciOiJ\` header. Log into Cal web app using owner of organization that was created after visiting \`/settings/organizations/new\`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.
Second, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.`;
@Controller({
path: "/v2/oauth-clients",
version: API_VERSIONS_VALUES,
@@ -61,7 +54,6 @@ export class OAuthClientsController {
@Post("/")
@HttpCode(HttpStatus.CREATED)
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
@DocsOperation({ description: AUTH_DOCUMENTATION })
@DocsCreatedResponse({
description: "Create an OAuth client",
type: CreateOAuthClientResponseDto,
@@ -90,7 +82,6 @@ export class OAuthClientsController {
@Get("/")
@HttpCode(HttpStatus.OK)
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
@DocsOperation({ description: AUTH_DOCUMENTATION })
async getOAuthClients(@GetOrgId() organizationId: number): Promise<GetOAuthClientsResponseDto> {
const clients = await this.oAuthClientsService.getOAuthClients(organizationId);
return { status: SUCCESS_STATUS, data: clients };
@@ -99,7 +90,6 @@ export class OAuthClientsController {
@Get("/:clientId")
@HttpCode(HttpStatus.OK)
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
@DocsOperation({ description: AUTH_DOCUMENTATION })
@UseGuards(OAuthClientGuard)
async getOAuthClientById(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
const client = await this.oAuthClientsService.getOAuthClientById(clientId);
@@ -109,7 +99,6 @@ export class OAuthClientsController {
@Get("/:clientId/managed-users")
@HttpCode(HttpStatus.OK)
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
@DocsOperation({ description: AUTH_DOCUMENTATION })
@UseGuards(OAuthClientGuard)
async getOAuthClientManagedUsersById(
@Param("clientId") clientId: string,
@@ -128,7 +117,6 @@ export class OAuthClientsController {
@Patch("/:clientId")
@HttpCode(HttpStatus.OK)
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
@DocsOperation({ description: AUTH_DOCUMENTATION })
@UseGuards(OAuthClientGuard)
async updateOAuthClient(
@Param("clientId") clientId: string,
@@ -142,7 +130,6 @@ export class OAuthClientsController {
@Delete("/:clientId")
@HttpCode(HttpStatus.OK)
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
@DocsOperation({ description: AUTH_DOCUMENTATION })
@UseGuards(OAuthClientGuard)
async deleteOAuthClient(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
this.logger.log(`Deleting OAuth Client with ID: ${clientId}`);
@@ -1,7 +1,11 @@
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { AuthModule } from "@/modules/auth/auth.module";
import { BillingModule } from "@/modules/billing/billing.module";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OAuthClientUsersController } from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller";
import { OAuthClientsController } from "@/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller";
@@ -16,6 +20,7 @@ import { OrganizationsModule } from "@/modules/organizations/organizations.modul
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
@@ -42,6 +47,11 @@ import { JwtService } from "@nestjs/jwt";
OAuthClientRepository,
TokensRepository,
OAuthFlowService,
CalendarsService,
CredentialsRepository,
AppsRepository,
CalendarsRepository,
SelectedCalendarsRepository,
OAuthClientUsersService,
OrganizationsTeamsService,
OAuthClientsService,
@@ -1,3 +1,4 @@
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { GetManagedUsersInput } from "@/modules/oauth-clients/controllers/oauth-client-users/inputs/get-managed-users.input";
@@ -5,18 +6,21 @@ import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
import { BadRequestException, ConflictException, Injectable, Logger } from "@nestjs/common";
import { User, CreationSource, PlatformOAuthClient } from "@prisma/client";
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
@Injectable()
export class OAuthClientUsersService {
private readonly logger = new Logger("OAuthClientUsersService");
constructor(
private readonly userRepository: UsersRepository,
private readonly tokensRepository: TokensRepository,
private readonly eventTypesService: EventTypesService_2024_04_15,
private readonly schedulesService: SchedulesService_2024_04_15
private readonly schedulesService: SchedulesService_2024_04_15,
private readonly calendarsService: CalendarsService
) {}
async createOAuthClientUser(oAuthClient: PlatformOAuthClient, body: CreateManagedUserInput) {
@@ -87,6 +91,13 @@ export class OAuthClientUsersService {
user.defaultScheduleId = defaultSchedule.id;
}
try {
this.logger.log(`Setting default calendars in db for user with id ${user.id}`);
await this.calendarsService.getCalendars(user.id);
} catch (err) {
this.logger.error(`Could not get calendars of new managed user with id ${user.id}`);
}
return {
user,
tokens: {
@@ -123,7 +134,9 @@ export class OAuthClientUsersService {
if (body.email) {
const emailWithOAuthId = OAuthClientUsersService.getOAuthUserEmail(oAuthClientId, body.email);
body.email = emailWithOAuthId;
const newUsername = slugify(emailWithOAuthId);
const [emailUser, emailDomain] = emailWithOAuthId.split("@");
const [domainName, TLD] = emailDomain.split(".");
const newUsername = slugify(`${emailUser}-${domainName}-${TLD}`);
await this.userRepository.updateUsername(userId, newUsername);
}
@@ -30,5 +30,7 @@ export class CreateDelegationCredentialInput {
})
@Expose()
@Type(() => Object)
serviceAccountKey!: GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput;
serviceAccountKey!: (GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput) & {
[key: string]: unknown;
};
}
@@ -38,6 +38,15 @@ export class OrganizationsDelegationCredentialService {
delegatedServiceAccountUser: User,
body: UpdateDelegationCredentialInput
) {
const delegationCredential =
await this.organizationsDelegationCredentialRepository.findByIdWithWorkspacePlatform(
delegationCredentialId
);
if (!delegationCredential) {
throw new NotFoundException(`DelegationCredential with id ${delegationCredentialId} not found`);
}
if (body.enabled !== undefined) {
await this.updateDelegationCredentialEnabled(
orgId,
@@ -49,15 +58,8 @@ export class OrganizationsDelegationCredentialService {
if (body.serviceAccountKey !== undefined) {
await this.updateDelegationCredentialServiceAccountKey(delegationCredentialId, body.serviceAccountKey);
}
const delegationCredential =
await this.organizationsDelegationCredentialRepository.findByIdWithWorkspacePlatform(
delegationCredentialId
);
if (!delegationCredential) {
throw new NotFoundException(`DelegationCredential with id ${delegationCredentialId} not found`);
}
return delegationCredential;
return { ...delegationCredential, enabled: body?.enabled ?? delegationCredential?.enabled };
}
async updateDelegationCredentialEnabled(
@@ -81,7 +81,7 @@ export class OrganizationsEventTypesController {
bodyEventType
);
const eventType = await this.organizationsEventTypesService.createTeamEventType(
const eventType = await this.organizationsEventTypesService.createOrganizationTeamEventType(
user,
teamId,
orgId,
@@ -212,7 +212,7 @@ export class OrganizationsEventTypesController {
bodyEventType
);
const eventType = await this.organizationsEventTypesService.updateTeamEventType(
const eventType = await this.organizationsEventTypesService.updateOrganizationTeamEventType(
eventTypeId,
teamId,
transformedBody,
@@ -581,6 +581,123 @@ describe("Organizations Event Types Endpoints", () => {
});
});
it("should be able to create phone-only event type", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: "Phone coding consultation",
slug: "phone-coding-consultation",
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
{
type: "organizersDefaultApp",
},
],
schedulingType: "COLLECTIVE",
hosts: [
{
userId: teammate1.id,
mandatory: true,
priority: "high",
},
],
bookingFields: [
{
type: "email",
required: false,
label: "Email",
hidden: true,
},
{
type: "phone",
slug: "attendeePhoneNumber",
required: true,
label: "Phone number",
hidden: false,
},
],
};
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data.bookingFields).toEqual([
{
isDefault: true,
type: "name",
slug: "name",
required: true,
disableOnPrefill: false,
},
{
isDefault: true,
type: "email",
slug: "email",
required: false,
label: "Email",
disableOnPrefill: false,
hidden: true,
},
{
isDefault: true,
type: "radioInput",
slug: "location",
required: false,
hidden: false,
},
{
isDefault: true,
type: "phone",
slug: "attendeePhoneNumber",
required: true,
hidden: false,
label: "Phone number",
disableOnPrefill: false,
},
{
isDefault: true,
type: "text",
slug: "title",
required: true,
disableOnPrefill: false,
hidden: true,
},
{
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "multiemail",
slug: "guests",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "textarea",
slug: "rescheduleReason",
required: false,
disableOnPrefill: false,
hidden: false,
},
]);
});
});
it("should be able to configure phone-only event type", async () => {
const body: UpdateTeamEventTypeInput_2024_06_14 = {
bookingFields: [
@@ -588,13 +705,14 @@ describe("Organizations Event Types Endpoints", () => {
type: "email",
required: false,
label: "Email",
hidden: true,
},
{
type: "phone",
slug: "attendeePhoneNumber",
required: true,
label: "Phone number",
hidden: true,
hidden: false,
},
],
};
@@ -622,6 +740,7 @@ describe("Organizations Event Types Endpoints", () => {
required: false,
label: "Email",
disableOnPrefill: false,
hidden: true,
},
{
isDefault: true,
@@ -635,7 +754,7 @@ describe("Organizations Event Types Endpoints", () => {
type: "phone",
slug: "attendeePhoneNumber",
required: true,
hidden: true,
hidden: false,
label: "Phone number",
disableOnPrefill: false,
},
@@ -776,7 +895,14 @@ describe("Organizations Event Types Endpoints", () => {
expect(fetchedEventType.bookingFields).toEqual([
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{
isDefault: true,
required: true,
slug: "email",
type: "email",
disableOnPrefill: false,
hidden: false,
},
{
disableOnPrefill: false,
isDefault: true,
@@ -26,7 +26,7 @@ export class OrganizationsEventTypesService {
private readonly usersService: UsersService
) {}
async createTeamEventType(
async createOrganizationTeamEventType(
user: UserWithProfile,
teamId: number,
orgId: number,
@@ -48,7 +48,7 @@ export class OrganizationsEventTypesService {
},
});
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user, true);
}
async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) {
@@ -91,13 +91,13 @@ export class OrganizationsEventTypesService {
return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes(orgId, skip, take);
}
async updateTeamEventType(
async updateOrganizationTeamEventType(
eventTypeId: number,
teamId: number,
body: TransformedUpdateTeamEventTypeInput,
user: UserWithProfile
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user);
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user, true);
}
async deleteTeamEventType(teamId: number, eventTypeId: number) {
@@ -37,11 +37,12 @@ export class OrganizationsMembershipRepository {
}
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
return this.dbWrite.prisma.membership.create({
data: { ...data, teamId: organizationId },
return this.dbWrite.prisma.membership.upsert({
create: { ...data, teamId: organizationId },
update: { role: data.role, accepted: data.accepted, disableImpersonation: data.disableImpersonation },
where: { userId_teamId: { userId: data.userId, teamId: organizationId } },
});
}
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
return this.dbWrite.prisma.membership.update({
data: { ...data },
@@ -75,6 +75,9 @@ describe("Slots 2024-09-04 Endpoints", () => {
const seatedEventTypeSlug = `slots-2024-09-04-seated-event-type-${randomString()}`;
let seatedEventType: EventType;
const variableLengthEventTypeSlug = `slots-2024-09-04-variable-length-event-type-${randomString()}`;
let variableLengthEventType: EventType;
let reservedSlot: ReserveSlotOutputData_2024_09_04;
beforeAll(async () => {
@@ -168,6 +171,17 @@ describe("Slots 2024-09-04 Endpoints", () => {
);
seatedEventType = seatedEvent;
const variableLengthEvent = await eventTypesRepositoryFixture.create(
{
title: "frisbee match",
slug: `slots-2024-09-04-variable-length-event-type-${randomString()}`,
length: 15,
metadata: { multipleDuration: [15, 30, 45, 60] },
},
user.id
);
variableLengthEventType = variableLengthEvent;
team = await teamRepositoryFixture.create({
name: `slots-2024-09-04-team-${randomString()}`,
isOrganization: false,
@@ -1246,6 +1260,90 @@ describe("Slots 2024-09-04 Endpoints", () => {
await bookingsRepositoryFixture.deleteById(booking.id);
});
describe("variable length", () => {
it("should not be able to reserve a slot for variable length event type with invalid duration", async () => {
const slotStartTime = "2050-09-05T10:00:00.000Z";
const reserveResponse = await request(app.getHttpServer())
.post(`/v2/slots/reservations`)
.send({
eventTypeId: variableLengthEventType.id,
slotStart: slotStartTime,
slotDuration: 1000,
})
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
.expect(400);
expect(reserveResponse.body.error.message).toEqual(
"Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: 15, 30, 45, 60"
);
});
it("should reserve a slot with slot duration for variable event type length", async () => {
// note(Lauris): mock current date to test slots release time
const now = "2049-09-05T12:00:00.000Z";
const newDate = DateTime.fromISO(now, { zone: "UTC" }).toJSDate();
advanceTo(newDate);
const slotDuration = 60;
const slotStartTime = "2050-09-05T10:00:00.000Z";
const reserveResponse = await request(app.getHttpServer())
.post(`/v2/slots/reservations`)
.send({
eventTypeId: variableLengthEventType.id,
slotStart: slotStartTime,
slotDuration,
})
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
.expect(201);
const reserveResponseBody: ReserveSlotOutputResponse_2024_09_04 = reserveResponse.body;
expect(reserveResponseBody.status).toEqual(SUCCESS_STATUS);
const responseReservedSlot: ReserveSlotOutputData_2024_09_04 = reserveResponseBody.data;
expect(responseReservedSlot.reservationUid).toBeDefined();
expect(responseReservedSlot.eventTypeId).toEqual(variableLengthEventType.id);
expect(responseReservedSlot.slotStart).toEqual(slotStartTime);
expect(responseReservedSlot.slotDuration).toEqual(slotDuration);
expect(responseReservedSlot.slotEnd).toEqual(
DateTime.fromISO(slotStartTime, { zone: "UTC" }).plus({ minutes: slotDuration }).toISO()
);
expect(responseReservedSlot.reservationDuration).toEqual(5);
if (!responseReservedSlot.reservationUid) {
throw new Error("Reserved slot uid is undefined");
}
const response = await request(app.getHttpServer())
.get(
`/v2/slots?eventTypeId=${variableLengthEventType.id}&start=2050-09-05&end=2050-09-09&duration=60`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
.expect(200);
const responseBody: GetSlotsOutput_2024_09_04 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const slots = responseBody.data;
expect(slots).toBeDefined();
const days = Object.keys(slots);
expect(days.length).toEqual(5);
const expectedSlotsUTC2050_09_05 = expectedSlotsUTC["2050-09-05"].filter(
(slot) => slot.start !== slotStartTime
);
expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 });
const dbSlot = await selectedSlotsRepositoryFixture.getByUid(reservedSlot.reservationUid);
expect(dbSlot).toBeDefined();
if (dbSlot) {
const dbReleaseAt = DateTime.fromJSDate(dbSlot.releaseAt, { zone: "UTC" }).toISO();
const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 5 }).toISO();
expect(dbReleaseAt).toEqual(expectedReleaseAt);
expect(responseReservedSlot.reservationUntil).toEqual(expectedReleaseAt);
}
clear();
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await userRepositoryFixture.deleteByEmail(unrelatedUser.email);
@@ -47,8 +47,12 @@ import { ApiResponse } from "@calcom/platform-types";
@DocsTags("Slots")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-09-04\``,
description: `Must be set to ${VERSION_2024_09_04}`,
example: VERSION_2024_09_04,
required: true,
schema: {
default: VERSION_2024_09_04,
},
})
export class SlotsController_2024_09_04 {
constructor(private readonly slotsService: SlotsService_2024_09_04) {}
@@ -21,9 +21,11 @@ import { getAvailableSlots } from "@calcom/platform-libraries/slots";
import { GetSlotsInput_2024_09_04, ReserveSlotInput_2024_09_04 } from "@calcom/platform-types";
import { EventType } from "@calcom/prisma/client";
const eventTypeMetadataSchema = z.object({
multipleDuration: z.number().array().optional(),
});
const eventTypeMetadataSchema = z
.object({
multipleDuration: z.number().array().optional(),
})
.nullable();
const DEFAULT_RESERVATION_DURATION = 5;
@@ -89,17 +91,8 @@ export class SlotsService_2024_09_04 {
throw new BadRequestException("Invalid start date");
}
const metadata = eventTypeMetadataSchema.parse(eventType);
if (
input.slotDuration &&
metadata.multipleDuration &&
!metadata.multipleDuration.includes(input.slotDuration)
) {
throw new BadRequestException(
`Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: ${metadata.multipleDuration.join(
", "
)}`
);
if (input.slotDuration) {
this.validateSlotDuration(eventType, input.slotDuration);
}
const endDate = startDate.plus({ minutes: input.slotDuration ?? eventType.length });
@@ -160,6 +153,23 @@ export class SlotsService_2024_09_04 {
return this.slotsOutputService.getReservationSlotCreated(slot, reservationDuration);
}
validateSlotDuration(eventType: EventType, inputSlotDuration: number) {
const eventTypeMetadata = eventTypeMetadataSchema.parse(eventType.metadata);
if (!eventTypeMetadata?.multipleDuration) {
throw new BadRequestException(
"You passed 'slotDuration' but this event type is not a variable length event type."
);
}
if (!eventTypeMetadata.multipleDuration.includes(inputSlotDuration)) {
throw new BadRequestException(
`Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: ${eventTypeMetadata.multipleDuration.join(
", "
)}`
);
}
}
async canSpecifyCustomReservationDuration(authUserId: number, eventType: EventType) {
if (eventType.userId) {
return await this.canSpecifyCustomReservationDurationIndividualEvent(authUserId, eventType.userId);
@@ -214,17 +224,8 @@ export class SlotsService_2024_09_04 {
throw new BadRequestException("Invalid start date");
}
const metadata = eventTypeMetadataSchema.parse(eventType);
if (
input.slotDuration &&
metadata.multipleDuration &&
!metadata.multipleDuration.includes(input.slotDuration)
) {
throw new BadRequestException(
`Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: ${metadata.multipleDuration.join(
", "
)}`
);
if (input.slotDuration) {
this.validateSlotDuration(eventType, input.slotDuration);
}
const endDate = startDate.plus({ minutes: input.slotDuration ?? eventType.length });
@@ -167,6 +167,56 @@ describe("Organizations Event Types Endpoints", () => {
return request(app.getHttpServer()).post(`/v2/teams/${team.id}/event-types`).send(body).expect(404);
});
it("should not be able to create phone-only event type", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: "Phone coding consultation",
slug: "phone-coding-consultation",
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
{
type: "organizersDefaultApp",
},
],
schedulingType: "COLLECTIVE",
hosts: [
{
userId: teamMember1.id,
},
{
userId: teamMember2.id,
},
],
bookingFields: [
{
type: "email",
required: false,
label: "Email",
hidden: true,
},
{
type: "phone",
slug: "attendeePhoneNumber",
required: true,
label: "Phone number",
hidden: false,
},
],
};
const response = await request(app.getHttpServer())
.post(`/v2/teams/${team.id}/event-types`)
.send(body)
.expect(400);
expect(response.body.error.message).toBe(
"checkIsEmailUserAccessible - Email booking field must be required and visible"
);
});
it("should create a collective team event-type", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: `teams-event-types-collective-${randomString()}`,
@@ -181,7 +181,8 @@ export class TeamsEventTypesController {
eventTypeId,
teamId,
transformedBody,
user
user,
false
);
return {
@@ -30,6 +30,10 @@ export class TeamsEventTypesService {
teamId: number,
body: TransformedCreateTeamEventTypeInput
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
// note(Lauris): once phone only event types / bookings are enabled for simple users remove checkHasUserAccessibleEmailBookingField check
if (body.bookingFields) {
this.eventTypesService.checkHasUserAccessibleEmailBookingField(body.bookingFields);
}
const eventTypeUser = await this.getUserToCreateTeamEvent(user);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { hosts, children, destinationCalendar, ...rest } = body;
@@ -44,7 +48,7 @@ export class TeamsEventTypesService {
},
});
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user, false);
}
async validateEventTypeExists(teamId: number, eventTypeId: number) {
@@ -105,8 +109,14 @@ export class TeamsEventTypesService {
eventTypeId: number,
teamId: number,
body: TransformedUpdateTeamEventTypeInput,
user: UserWithProfile
user: UserWithProfile,
// note(Lauris): once phone only event types / bookings are enabled for simple users remove isOrg parameter (right now only organization team event types support hidden / non-required email field)
isOrg: boolean
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
if (!isOrg && body.bookingFields) {
// note(Lauris): once phone only event types / bookings are enabled for simple users remove checkHasUserAccessibleEmailBookingField check
this.eventTypesService.checkHasUserAccessibleEmailBookingField(body.bookingFields);
}
await this.validateEventTypeExists(teamId, eventTypeId);
const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user);
await updateEventType({
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -22,7 +22,8 @@
"@calcom/platform-libraries/event-types": ["../../../packages/platform/libraries/event-types.ts"],
"@calcom/platform-libraries/slots": ["../../../packages/platform/libraries/slots.ts"],
"@calcom/platform-libraries/emails": ["../../../packages/platform/libraries/emails.ts"],
"@calcom/platform-libraries/schedules": ["../../../packages/platform/libraries/schedules.ts"]
"@calcom/platform-libraries/schedules": ["../../../packages/platform/libraries/schedules.ts"],
"@calcom/platform-libraries/app-store": ["../../../packages/platform/libraries/app-store.ts"]
},
"incremental": true,
"skipLibCheck": true,
@@ -8,4 +8,8 @@ export const generateMetadata = async () =>
(t) => t("create_your_org_description")
);
export default EnterprisePage;
const ServerPageWrapper = async () => {
return <EnterprisePage />;
};
export default ServerPageWrapper;
@@ -174,7 +174,7 @@ const organizationAdminKeys = [
const useTabs = ({ isDelegationCredentialEnabled }: { isDelegationCredentialEnabled: boolean }) => {
const session = useSession();
const { data: user } = trpc.viewer.me.useQuery({ includePasswordAdded: true });
const { data: user } = trpc.viewer.me.get.useQuery({ includePasswordAdded: true });
const orgBranding = useOrgBranding();
const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN;
const isOrgAdminOrOwner =
@@ -3,7 +3,7 @@ import { getTranslate } from "app/_utils";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import CreateNewOutOfOfficeEntryButton from "@calcom/features/settings/outOfOffice/CreateNewOutOfOfficeEntryButton";
import { OutOfOfficeEntriesList } from "@calcom/features/settings/outOfOffice/OutOfOfficeEntriesList";
import OutOfOfficeEntriesList from "@calcom/features/settings/outOfOffice/OutOfOfficeEntriesList";
import { OutOfOfficeToggleGroup } from "@calcom/features/settings/outOfOffice/OutOfOfficeToggleGroup";
export const generateMetadata = async () =>
@@ -3,11 +3,10 @@ import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/[id]/about-view";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -3,11 +3,10 @@ import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/[id]/add-teams-view";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -0,0 +1,29 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { cookies } from "next/headers";
import { headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("handover_onboarding_page_title"),
(t) => t("handover_onboarding_page_description")
);
const getData = withAppDirSsr(getServerSideProps);
const ServerPage = async ({ params, searchParams }: PageProps) => {
await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
return (
<LayoutWrapper>
<LegacyPage />
</LayoutWrapper>
);
};
export default ServerPage;
@@ -3,11 +3,10 @@ import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/[id]/onboard-members-view";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboard-members-view";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -7,7 +7,6 @@ import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequir
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
import { type inferSSRProps } from "@lib/types/inferSSRProps";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/create-new-view";
@@ -17,7 +16,11 @@ export const generateMetadata = async () =>
(t) => t("organizations_description")
);
const getData = withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServerSideProps);
type Props = {
isOrg: boolean;
};
const getData = withAppDirSsr<Props>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: PageProps) => {
await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
@@ -0,0 +1,28 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/payment-status-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("organization_onboarding_status"),
(t) => t("organization_onboarding_status_description")
);
const getData = withAppDirSsr(getServerSideProps);
const ServerPage = async ({ params, searchParams }: PageProps) => {
await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
return (
<LayoutWrapper>
<LegacyPage />
</LayoutWrapper>
);
};
export default ServerPage;
@@ -7,17 +7,20 @@ import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequir
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
import { type inferSSRProps } from "@lib/types/inferSSRProps";
import LegacyPage, { LayoutWrapper } from "~/settings/platform/new/create-new-view";
type Props = {
isOrg: boolean;
};
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("set_up_your_platform_organization"),
(t) => t("platform_organization_description")
);
const getData = withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServerSideProps);
const getData = withAppDirSsr<Props>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: PageProps) => {
await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
@@ -8,4 +8,8 @@ export const generateMetadata = async () =>
(t) => t("no_meeting_found")
);
export default NoMeetingFound;
const ServerPage = async () => {
return <NoMeetingFound />;
};
export default ServerPage;
+1 -1
View File
@@ -1,4 +1,4 @@
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
import { createTRPCReact } from "@trpc/react-query";
+1 -1
View File
@@ -7,7 +7,7 @@ import { useState } from "react";
import superjson from "superjson";
import { ENDPOINTS } from "@calcom/trpc/react/shared";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
import { httpBatchLink, httpLink, loggerLink, splitLink, TRPCClientError } from "@trpc/client";
@@ -46,6 +46,4 @@ async function handler(req: NextRequest) {
}
}
const postHandler = defaultResponderForAppDir(handler);
export { postHandler as POST };
export const POST = defaultResponderForAppDir(handler);
+3 -2
View File
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
@@ -14,5 +15,5 @@ async function handler(req: NextRequest) {
return NextResponse.json({ username: account.name }, { status: 201 });
}
export { handler as GET, handler as POST };
export const GET = defaultResponderForAppDir(handler);
export const POST = defaultResponderForAppDir(handler);
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { parseUrlFormData } from "app/api/parseRequestData";
import jwt from "jsonwebtoken";
import { NextResponse } from "next/server";
@@ -7,7 +8,7 @@ import prisma from "@calcom/prisma";
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
import type { OAuthTokenPayload } from "@calcom/types/oauth";
export async function POST(req: NextRequest) {
async function handler(req: NextRequest) {
const { client_id, client_secret, grant_type } = await parseUrlFormData(req);
if (!client_id || !client_secret) {
@@ -63,3 +64,5 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ access_token }, { status: 200 });
}
export const POST = defaultResponderForAppDir(handler);
+4 -1
View File
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { parseUrlFormData } from "app/api/parseRequestData";
import jwt from "jsonwebtoken";
import { NextResponse } from "next/server";
@@ -7,7 +8,7 @@ import prisma from "@calcom/prisma";
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
import type { OAuthTokenPayload } from "@calcom/types/oauth";
export async function POST(req: NextRequest) {
async function handler(req: NextRequest) {
const { code, client_id, client_secret, grant_type, redirect_uri } = await parseUrlFormData(req);
if (grant_type !== "authorization_code") {
return NextResponse.json({ message: "grant_type invalid" }, { status: 400 });
@@ -88,3 +89,5 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ access_token, refresh_token }, { status: 200 });
}
export const POST = defaultResponderForAppDir(handler);
+1 -3
View File
@@ -36,6 +36,4 @@ async function handler(req: NextRequest) {
}
}
const getHandler = defaultResponderForAppDir(handler);
export { getHandler as GET };
export const GET = defaultResponderForAppDir(handler);
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { parseRequestData } from "app/api/parseRequestData";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
@@ -75,4 +76,4 @@ async function expireResetPasswordRequest(rawRequestId: string) {
});
}
export { handler as POST };
export const POST = defaultResponderForAppDir(handler);
@@ -22,8 +22,6 @@ async function handler(req: NextRequest) {
}
}
const getHandler = defaultResponderForAppDir(handler);
export { getHandler as GET };
export const GET = defaultResponderForAppDir(handler);
export const dynamic = "force-dynamic";
+1 -3
View File
@@ -20,6 +20,4 @@ async function handler(req: NextRequest) {
return NextResponse.json({ message: "No redirect URL provided" }, { status: 400 });
}
const postHandler = defaultResponderForAppDir(handler);
export { postHandler as POST };
export const POST = defaultResponderForAppDir(handler);
+1 -3
View File
@@ -12,6 +12,4 @@ async function handler(req: NextRequest) {
return NextResponse.json(tokenResponse);
}
const postHandler = defaultResponderForAppDir(handler);
export { postHandler as POST };
export const POST = defaultResponderForAppDir(handler);
+1 -3
View File
@@ -31,8 +31,6 @@ async function handler(req: NextRequest) {
return NextResponse.json(userInfo);
}
const getHandler = defaultResponderForAppDir(handler);
export { getHandler as GET };
export const GET = defaultResponderForAppDir(handler);
export const dynamic = "force-dynamic";
+1 -3
View File
@@ -59,6 +59,4 @@ async function handler(req: NextRequest) {
return NextResponse.json({ message: "First admin user created successfully." });
}
const postHandler = defaultResponderForAppDir(handler);
export { postHandler as POST };
export const POST = defaultResponderForAppDir(handler);
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { parseRequestData } from "app/api/parseRequestData";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
@@ -13,7 +14,7 @@ import { IdentityProvider } from "@calcom/prisma/client";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function POST(req: NextRequest) {
async function handler(req: NextRequest) {
const body = await parseRequestData(req);
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
@@ -114,3 +115,5 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ message: "Two factor disabled" });
}
export const POST = defaultResponderForAppDir(handler);
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { parseRequestData } from "app/api/parseRequestData";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
@@ -11,7 +12,7 @@ import prisma from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function POST(req: NextRequest) {
async function postHandler(req: NextRequest) {
const body = await parseRequestData(req);
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
@@ -67,3 +68,5 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ message: "Two-factor enabled" });
}
export const POST = defaultResponderForAppDir(postHandler);
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { parseRequestData } from "app/api/parseRequestData";
import crypto from "crypto";
import { cookies, headers } from "next/headers";
@@ -15,7 +16,7 @@ import { IdentityProvider } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function POST(req: NextRequest) {
async function postHandler(req: NextRequest) {
const body = await parseRequestData(req);
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
@@ -81,3 +82,5 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ secret, keyUri, dataUri, backupCodes });
}
export const POST = defaultResponderForAppDir(postHandler);
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
@@ -105,4 +106,6 @@ async function deleteHandler(req: NextRequest) {
return NextResponse.json({ message: "Calendar Selection Saved" });
}
export { deleteHandler as DELETE, postHandler as POST, getHandler as GET };
export const POST = defaultResponderForAppDir(postHandler);
export const DELETE = defaultResponderForAppDir(deleteHandler);
export const GET = defaultResponderForAppDir(getHandler);
+4 -5
View File
@@ -4,7 +4,7 @@ import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
import { AVATAR_FALLBACK, WEBAPP_URL } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
const querySchema = z.object({
@@ -47,7 +47,8 @@ async function handler(req: NextRequest, { params }: { params: Params }) {
img = data;
} catch (e) {
// If anything goes wrong or avatar is not found, use default avatar
return NextResponse.redirect(AVATAR_FALLBACK, 302);
const url = new URL(AVATAR_FALLBACK, WEBAPP_URL).toString();
return NextResponse.redirect(url, 302);
}
const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", "");
@@ -63,6 +64,4 @@ async function handler(req: NextRequest, { params }: { params: Params }) {
});
}
const getHandler = defaultResponderForAppDir(handler);
export { getHandler as GET };
export const GET = defaultResponderForAppDir(handler);
+2 -4
View File
@@ -26,7 +26,5 @@ async function handler(req: NextRequest) {
return NextResponse.json(result, { status: statusCode });
}
const deleteHandler = defaultResponderForAppDir(handler);
const postHandler = defaultResponderForAppDir(handler);
export { deleteHandler as DELETE, postHandler as POST };
export const DELETE = defaultResponderForAppDir(handler);
export const POST = defaultResponderForAppDir(handler);
+15 -3
View File
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
@@ -11,7 +12,7 @@ import { BookingStatus, ReminderType } from "@calcom/prisma/enums";
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
export async function POST(request: NextRequest) {
async function postHandler(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
@@ -54,6 +55,8 @@ export async function POST(request: NextRequest) {
locale: true,
timeZone: true,
destinationCalendar: true,
isPlatformManaged: true,
platformOAuthClients: { select: { id: true, areEmailsEnabled: true } },
},
},
eventType: {
@@ -69,11 +72,18 @@ export async function POST(request: NextRequest) {
},
});
const bookingsToRemind = bookings.filter(
(booking) =>
!booking.user ||
!booking.user.isPlatformManaged ||
(booking.user.isPlatformManaged && Boolean(booking.user.platformOAuthClients?.[0]?.areEmailsEnabled))
);
const reminders = await prisma.reminderMail.findMany({
where: {
reminderType: ReminderType.PENDING_BOOKING_CONFIRMATION,
referenceId: {
in: bookings.map((b) => b.id),
in: bookingsToRemind.map((b) => b.id),
},
elapsedMinutes: {
gte: interval,
@@ -81,7 +91,7 @@ export async function POST(request: NextRequest) {
},
});
for (const booking of bookings.filter((b) => !reminders.some((r) => r.referenceId == b.id))) {
for (const booking of bookingsToRemind.filter((b) => !reminders.some((r) => r.referenceId == b.id))) {
const { user } = booking;
const name = user?.name || user?.username;
if (!user || !name || !user.timeZone) {
@@ -145,3 +155,5 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ notificationsSent });
}
export const POST = defaultResponderForAppDir(postHandler);
@@ -1,9 +1,10 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import prisma from "@calcom/prisma";
export async function POST(request: NextRequest) {
async function postHandler(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (![process.env.CRON_API_KEY, `Bearer ${process.env.CRON_SECRET}`].includes(`${apiKey}`)) {
@@ -21,3 +22,5 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ ok: true, count: deleted.count });
}
export const POST = defaultResponderForAppDir(postHandler);
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
@@ -20,7 +21,7 @@ const travelScheduleSelect = {
},
};
export async function POST(request: NextRequest) {
async function postHandler(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
@@ -167,3 +168,5 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ timeZonesChanged });
}
export const POST = defaultResponderForAppDir(postHandler);
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
@@ -9,7 +10,7 @@ const querySchema = z.object({
page: z.coerce.number().min(0).optional().default(0),
});
export async function POST(request: NextRequest) {
async function postHandler(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
@@ -51,3 +52,5 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ ok: true });
}
export const POST = defaultResponderForAppDir(postHandler);
@@ -1,4 +1,5 @@
import type { Prisma } from "@prisma/client";
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
@@ -13,7 +14,7 @@ const querySchema = z.object({
page: z.coerce.number().min(0).optional().default(0),
});
export async function POST(request: NextRequest) {
async function postHandler(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
@@ -313,3 +314,5 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ ok: true });
}
export const POST = defaultResponderForAppDir(postHandler);
+4 -1
View File
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
@@ -15,7 +16,7 @@ const log = logger.getSubLogger({
* syncAppMeta makes sure any app metadata that has been replicated into the database
* remains synchronized with any changes made to the app config files.
*/
export async function POST(request: NextRequest) {
async function postHandler(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
@@ -62,3 +63,5 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ ok: true });
}
export const POST = defaultResponderForAppDir(postHandler);
@@ -1,10 +1,11 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { handleWebhookScheduledTriggers } from "@calcom/features/webhooks/lib/handleWebhookScheduledTriggers";
import prisma from "@calcom/prisma";
export async function POST(req: NextRequest) {
async function postHandler(req: NextRequest) {
const apiKey = req.headers.get("authorization") || req.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
@@ -15,3 +16,5 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ ok: true });
}
export const POST = defaultResponderForAppDir(postHandler);
@@ -1,3 +1,5 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { handler } from "@calcom/features/ee/workflows/api/scheduleEmailReminders";
export { handler as POST };
export const POST = defaultResponderForAppDir(handler);
@@ -1,3 +1,5 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { handler } from "@calcom/features/ee/workflows/api/scheduleSMSReminders";
export { handler as POST };
export const POST = defaultResponderForAppDir(handler);
@@ -1,3 +1,5 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { handler } from "@calcom/features/ee/workflows/api/scheduleWhatsappReminders";
export { handler as POST };
export const POST = defaultResponderForAppDir(handler);
+4 -1
View File
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { NextResponse } from "next/server";
import { renderEmail } from "@calcom/emails";
@@ -7,7 +8,7 @@ import { getTranslation } from "@calcom/lib/server/i18n";
/**
* This API endpoint is used for development purposes to preview email templates
*/
export async function GET() {
async function getHandler() {
// Only allow in development mode
if (IS_PRODUCTION) {
return new NextResponse("Only for development purposes", {
@@ -73,3 +74,5 @@ export async function GET() {
return response;
}
export const GET = defaultResponderForAppDir(getHandler);
+4 -1
View File
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
@@ -6,7 +7,7 @@ import { FUTURE_ROUTES_OVERRIDE_COOKIE_NAME as COOKIE_NAME } from "@calcom/lib/c
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function GET() {
async function getHandler() {
const headersList = headers();
const cookiesList = cookies();
const legacyReq = buildLegacyRequest(headersList, cookiesList);
@@ -35,3 +36,5 @@ export async function GET() {
return response;
}
export const GET = defaultResponderForAppDir(getHandler);
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
@@ -7,7 +8,7 @@ import prisma from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function POST() {
async function postHandler() {
const headersList = headers();
const cookiesList = cookies();
const legacyReq = buildLegacyRequest(headersList, cookiesList);
@@ -51,3 +52,5 @@ export async function POST() {
return NextResponse.json({ shortLink });
}
export const POST = defaultResponderForAppDir(postHandler);
+4 -1
View File
@@ -1,7 +1,8 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { headers } from "next/headers";
import { NextResponse } from "next/server";
export async function GET() {
async function getHandler() {
const headersList = headers();
const country = headersList.get("x-vercel-ip-country") || "Unknown";
@@ -10,3 +11,5 @@ export async function GET() {
return response;
}
export const GET = defaultResponderForAppDir(getHandler);
+4 -1
View File
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import crypto from "node:crypto";
@@ -6,7 +7,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function GET() {
async function getHandler() {
const headersList = headers();
const cookiesList = cookies();
const legacyReq = buildLegacyRequest(headersList, cookiesList);
@@ -28,3 +29,5 @@ export async function GET() {
return NextResponse.json({ hash });
}
export const GET = defaultResponderForAppDir(getHandler);
+4 -1
View File
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
@@ -162,7 +163,7 @@ async function getTeamLogos(subdomain: string, isValidOrgDomain: boolean) {
/**
* This API endpoint is used to serve the logo associated with a team if no logo is found we serve our default logo
*/
export async function GET(request: NextRequest) {
async function getHandler(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const parsedQuery = logoApiSchema.parse(Object.fromEntries(searchParams.entries()));
@@ -217,3 +218,5 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: "Failed fetching logo" }, { status: 404 });
}
}
export const GET = defaultResponderForAppDir(getHandler);
+4 -1
View File
@@ -1,3 +1,4 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
@@ -8,7 +9,7 @@ import { buildLegacyRequest } from "@lib/buildLegacyCtx";
let isCold = true;
export async function GET() {
async function getHandler() {
const prePrismaDate = performance.now();
const prisma = (await import("@calcom/prisma")).default;
const preSessionDate = performance.now();
@@ -48,3 +49,5 @@ export async function GET() {
return response;
}
export const GET = defaultResponderForAppDir(getHandler);
+6 -2
View File
@@ -1,9 +1,13 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { NextResponse } from "next/server";
export async function GET() {
async function getHandler() {
return NextResponse.json({ message: "Please don't" }, { status: 400 });
}
export async function POST() {
async function postHandler() {
return NextResponse.json({ message: "Please don't" }, { status: 400 });
}
export const GET = defaultResponderForAppDir(getHandler);
export const POST = defaultResponderForAppDir(postHandler);
+6
View File
@@ -5,6 +5,7 @@ import { NextResponse } from "next/server";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { IS_PLAIN_CHAT_ENABLED } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -20,6 +21,11 @@ const responseSchema = z.object({
});
async function handler() {
// Early return if Plain Chat is not enabled
if (!IS_PLAIN_CHAT_ENABLED) {
return NextResponse.json({ error: "Plain Chat is not enabled" }, { status: 404 });
}
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized - No session email found" }, { status: 401 });
@@ -1,11 +1,14 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { createHmac } from "crypto";
import { headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getRoomNameFromRecordingId, getBatchProcessorJobAccessLink } from "@calcom/app-store/dailyvideo/lib";
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
import { sendDailyVideoTranscriptEmails } from "@calcom/emails";
import {
sendDailyVideoRecordingEmails,
sendDailyVideoTranscriptEmails,
} from "@calcom/emails/daily-video-emails";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
@@ -48,7 +51,7 @@ const getDownloadLinkOfCalVideo = async (recordingId: string) => {
return downloadLink;
};
export async function POST(request: NextRequest) {
export async function postHandler(request: NextRequest) {
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
return NextResponse.json({ message: "No SendGrid API key or email" }, { status: 405 });
}
@@ -225,3 +228,5 @@ export async function POST(request: NextRequest) {
}
}
}
export const POST = defaultResponderForAppDir(postHandler);
@@ -1,4 +1,5 @@
import type { DirectorySyncEvent, DirectorySyncRequest } from "@boxyhq/saml-jackson";
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
@@ -51,23 +52,23 @@ const handleEvents = async (event: DirectorySyncEvent) => {
};
// This is the handler for the SCIM API requests
export async function GET(request: NextRequest, { params }: { params: { directory?: string[] } }) {
async function getHandler(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "GET", params.directory);
}
export async function POST(request: NextRequest, { params }: { params: { directory?: string[] } }) {
async function postHandler(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "POST", params.directory);
}
export async function PUT(request: NextRequest, { params }: { params: { directory?: string[] } }) {
async function putHandler(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "PUT", params.directory);
}
export async function PATCH(request: NextRequest, { params }: { params: { directory?: string[] } }) {
async function patchHandler(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "PATCH", params.directory);
}
export async function DELETE(request: NextRequest, { params }: { params: { directory?: string[] } }) {
async function deleteHandler(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "DELETE", params.directory);
}
@@ -139,3 +140,8 @@ async function handleScimRequest(request: NextRequest, method: string, directory
return NextResponse.json(data, { status });
}
export const GET = defaultResponderForAppDir(getHandler);
export const POST = defaultResponderForAppDir(postHandler);
export const PATCH = defaultResponderForAppDir(patchHandler);
export const DELETE = defaultResponderForAppDir(deleteHandler);
export const PUT = defaultResponderForAppDir(putHandler);

Some files were not shown because too many files have changed in this diff Show More