Compare commits

...
78 Commits
Author SHA1 Message Date
Keith Williams 0b719ac070 chore: v4.4.2 (#16278)
release.yml / release (push) Canceled after 0s
2024-08-20 22:54:22 +09:00
Anik Dhabal BabuandSomay Chauhan baa9045af7 feat: ability to add guests via app.cal.com/bookings (#14740)
* feat: ability to add guests via app.cal.com/bookings

* fix: some update

* fix: minor issue

* fix: final update

* update

* update

* add requested changes

* fix type error

* small update

* final update

* fix type error

* fix location

* update calender event

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-08-20 09:26:19 +00:00
b004587262 fix: textarea ux in Editor.tsx #16225 (#16231)
* fix: textarea ux in Editor.tsx #16225

* fix: textarea ux in Editor.tsx #16225

* fix: textarea ux in Editor.tsx #16225

* fix: textarea ux in Editor.tsx #16225

* fix: textarea ux in Editor.tsx #16225

* fix and update

* remove space

---------

Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-08-20 08:39:33 +00:00
Sy Sagar 8a7beff339 style(Booker): overflow on timezone Select (#16242) 2024-08-20 08:26:53 +00:00
Shreya AgarwalandAmit Sharma 9001eb7795 fix: fix app config same key values (#16215)
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-08-20 07:33:24 +00:00
Hichem Fantar 50c4578c2e feat: Add transition to hover background in multiple components (#16216)
* chore: Add transition to hover background in multiple components

* chore: Add transition to hover background in multiple components
2024-08-20 07:23:09 +00:00
4ff2ce90f9 fix: whatsapp message body sometimes editable (Issue #16067) (#16185)
* Fixing issue #16067

* Creating reusable Editor component for Editor

* fix editor issue

* remove log

* fix: unit test

* fix: unit test

* add unit test for the plugin and changes

---------

Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-08-20 16:19:24 +09:00
86f8c6c5fa fix: Close.com can't be installed (#16196)
* Fix for #16183 , [CAL-3969]

* Update index.tsx

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-08-20 16:18:20 +09:00
Vijay 0611bbd50e fix: #16079 hide settings/Admin API if not org and update link to sales (#16198)
* hide admin_api if not org and update link to sales

* update for org members

* updated back to orgAdmin check
2024-08-20 16:17:24 +09:00
Omar López b946abab02 fix: only keep latest e2e test results (#16273) 2024-08-19 23:23:06 +00:00
Omar López a091c60618 fix: uncaught types errors in invite members team handler (#16109) 2024-08-19 20:27:37 +00:00
Omar López bba7fc1a30 hotfix: wrong branch name for reports (#16271) 2024-08-19 20:18:15 +00:00
zomars 864c46736b hotfix: wrong branch name for reports
Signed-off-by: zomars <zomars@me.com>
2024-08-19 12:59:44 -07:00
4416709ff6 chore(availabilty-page): Bulk update modal (#16267)
* chore(Availability): Bulk update modal

* fix type error

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-08-19 21:34:16 +02:00
Vijay 591d42fc8f fix: #16150 update to fix seated event reschedule (#16163)
* update to fix seated event reschedule

* updated handleseats tests

* updated handleSeats timeslot

* updated test to expect error

* update to remove uid in where clause (isFirstSeat)
2024-08-19 22:03:35 +05:30
Syed Ali Shahbaz d73b84157a fix: v1 api payments endpoint fix (#16264)
* Update payment validation v1

* fix for [id]
2024-08-19 16:29:36 +00:00
680e748b7a feat: make the meeting duration time more visually toggleable (#15422)
* feat: make the meeting duration time more visually toggleable

* final_update

* Update dynamic-booking-pages.e2e.ts

* fix e2e

* fix: e2e test

* fixed

* update

* show selcted item at the time of mount

* fix type error

* Update Duration.tsx

* fix

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-08-19 14:25:02 +00:00
zerone0xandunknown 99b7aaa62d fix: Chinese traditional shows multiple durations of the event as a string (#16187)
* fix: Chinese traditional shows multiple durations of the event as a string

* revert

* make changes only in necessary files

---------

Co-authored-by: unknown <adhabal2002@gmail.com>
2024-08-19 19:34:30 +09:00
Omar López 3a01e6390f fix: type issues in attributes-edit-view (#16243) 2024-08-19 10:33:20 +00:00
dillonstreatorandunknown a6c25ab7d3 docs: update documentation references to include /docs base path (#16257)
* docs: update /doc references

* gix avilibilty docs link

---------

Co-authored-by: unknown <adhabal2002@gmail.com>
2024-08-19 10:21:50 +00:00
Rajiv Sahal df98274a8f update icons list (#16263) 2024-08-19 10:21:36 +00:00
Kaiwalya Koparkar b540a2ec45 update: Deploy to Elestio button updated (#16238) 2024-08-19 19:16:35 +09:00
sean-brydon 8e4459f2fe chore: missing ff attributes migration (#16229)
* add migration file

* add miration file
2024-08-19 10:09:25 +00:00
Carina WollendorferandCarinaWolli bc5a9f231f fix: POST v1/teams request body (#16262)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-08-19 08:28:56 +00:00
Carina WollendorferandCarinaWolli 88984f6175 fix: event name in attendee cancellation email (#16240)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-08-19 08:24:52 +00:00
fc592a17ac feat: Add 'Disable on Prefill' option for fields (#15715)
* feat:add prefilcheck check box in the field creation

* chore:formatted the li8 translations

* feat:add e2e test case for this feature

* refactor:reanmed the key from prefilledCheck to disableOnPrefill and moved the readOnly logic to formbuilderField

* chore:fixed the type check

* fix:if prefilled has error. do not disabble it

* chore:removed the e2e tests for prefill in e2e

* chore:revert back to old changes

* chore:remove unwanted code frome2e

* Support name field as well

* refactor:handled the feedback changes. still unit test cases pending

* feat: add unit tests for  disable field on prefill logic

* chore: resolved the type check and typos

* chore:renamed the files and type clean up

* Apply suggestions from code review

* chore:renaming test files and moving the disableonprefill hook to formbuilderFeild.

* refactor: add unit test for serach params and form values check and minor code formatting and file name changes

* fix: validating the input value partially at the intialising phase. so that form response values and  from ui values are same and making sure invalid data not sent to backend on submit.

* chore:fix the type check issue

* fix: field value valdiation at initilisation and add the  unit test cases for that change

* chore:add comments to the tests

* chore:resolve type check

* chore: moved the tests to the formBuilder

* Simplify codebase by ensuring select and multiselect changes the value when no option is found

* Relocate and refactor tests

* handle special cases

* Dont disable for dirty fields

* Update schema.prisma

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-08-18 17:34:59 +00:00
Udit Takkar a51524e673 fix: cal video bug fix and improvements (#16234) 2024-08-18 09:09:27 +00:00
Omar López cb6553c731 chore: v4.4.1 (#16246)
release.yml / release (push) Canceled after 0s
2024-08-16 10:34:36 -07:00
Omar López 101fea2c1c fix: cross domain SVG sprites (#16244) 2024-08-16 16:33:10 +00:00
Morgan aab9193e7b fix: rewrite to avoid cross origin issues with sprite.svg icons (#16239) 2024-08-16 15:24:43 +03:00
Morgan a6d2e46b3d fix: check profiles from user.profiles apiv2 (#16235) 2024-08-16 11:06:53 +00:00
25ae1493c3 fix: Duplicating org team event types not working #16199 (#16204)
* fix: Duplicating org team event types not working #16199

* fix type error

* Update eventtype.ts

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-08-16 12:24:26 +02:00
Carina WollendorferandCarinaWolli 56dcab2f6d fix padding (#16227)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-08-16 09:53:28 +00:00
Somay ChauhanandMorgan 8fbc76d59a feat: extend api/v2/event-types (#16128)
* feat: extended api/v2/event-types to add more attributes

- additional attributes added
* bookingLimits
* onlyShowFirstAvailableSlot
* durationLimits
* offsetStart
* bookingWindow

* add swagger docs

* type fix

* extend api/v2/teams/[teamId]/event-types

* added tests

* extend api/v2/orgs/[orgId]/teams/[teamid]/event-types

* update: added tests to get and update event-type as well

* rename variables

- bookingLimits -> bookingLimitsCount
- durationLimits -> bookingLimitsDuration

* refactor: rename bookingLimits and durationLimits, add descriptions

- Renamed `bookingLimits` to `bookingLimitsCount`
- Renamed `durationLimits` to `bookingLimitsDuration`
- Added descriptions with examples to `bookingLimitsCount` and `bookingLimitsDuration`

* feat: add unit tests for transformation logic in api-request and api-response

- Added tests for `transformApiEventTypeIntervalLimits`
- Added tests for `transformApiEventTypeFutureBookingLimits`

* fix: type errors

* feat: added custom validators for BookingLimitsCount and BookingLimitsDuration

* fix: unit-tests

* Update CHANGELOG.md

* refactor: update imports to use platform-libraries from @calcom/platform-libraries-1.2.3

* fix: prevent double transformation of bookingWindow and bookingFields

- resolving e2e test failures

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-08-16 09:25:34 +00:00
Joe Au-Yeung 894c0fc8a7 Change body from HTML to text (#16217) 2024-08-16 00:40:56 +01:00
Alex van AndelandOmar López 076550b384 chore: Release v4.4.0 (#16219)
release.yml / release (push) Canceled after 0s
Co-authored-by: Omar López <zomars@me.com>
2024-08-15 14:51:18 -07:00
08e1b0a9c8 feat: attributes (#15964)
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2024-08-15 14:49:05 -07:00
Omar López ef4deb1657 fix: pipeline permissions for label action (#16220) 2024-08-15 14:37:23 -07:00
f81074c4f5 feat: destination calendar settings for CalendarSettings atom (#16133)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-08-15 14:31:53 -07:00
Omar López 5df14c344e refactor: use WebhookService in payments router (#16108) 2024-08-15 13:03:07 -07:00
cd311f076c feat: Round Robin Weights (#15558)
* WIP

* add frontend

* backend to update weight

* UI improvements

* WIP weight algorithm

* enable weights switch + algorithm improvements

* fix weightDescription

* clean up code

* remove OOOEntryHost from schema.prisma

* implement logic (not yet tested)

* add tests for weight algorithm

* add test with weight adjustment

* finish unit tests

* fix type error

* fix type error

* add migration

* fix type error

* fix event type update handler

* fix failing test

* UI fixes for saving hosts

* make sure weightAdjustment is not lost on host changes

* fix weightadjustment for new hosts

* add weightAdjustment to availableUsers

* fix type errors

* fix default value for weight

* make weight and weightAdjustment optional

* fix type errors from schema changes

* type fix

* clean up code

* improve comments

* remove comment

* clean up code

* add tests & weight adjustment improvments

* better variable naming

* fixes for weight adjustments

* make weightAdjustments proportional to weights

* fix previous host weight adjustments

* improved tests for weight adjustments

* save weight and priority + sort hosts correctly

* fix type error

* code clean up

* remove console.log

* use BookingRepository to fetch bookings of users

* use BookingRepository to fetch bookings in getLuckyUser

* fix type errors

* fix weightAdjustment if changed from fixed to rr host

* disable weights when 'assign all' is enabled

* typo

* allow 0 weight

* set min (and max) for weight and priority

* use useWatch

* code clean up

* fix type error

* only count accepted bookings for RR

* fix type error

* improve data fetching of bookings

* only filter bookings of availableUsers

* code clean up form feedback

* fix tests

* don't count no show bookings

* code clean up

* choose user with highest weight

* use one reduce instead of two

* use reduce instead of filter and map

* don't show weights toggle when 'assign all' is enabled

* design fixes

* fix type errors

* fix: type check

* Update packages/features/eventtypes/components/AddMembersWithSwitch.tsx

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-08-15 15:01:15 +01:00
Morgan d60f165223 fix: use platform me typing (#16214) 2024-08-15 11:31:26 +00:00
Morgan 8477128605 fix: platform me data for dashboard (#16210) 2024-08-15 10:12:31 +00:00
Morgan 84606543ae chore: preserve non-org dashboard for platform users (#16181) 2024-08-14 15:11:46 -07:00
Omar López a11242e93d fix: missing pipeline dependecies (#16205) 2024-08-14 21:41:46 +00:00
Omar López 5939a99aca test: merge all e2e reports (#16202) 2024-08-14 19:44:10 +00:00
Alex van AndelandOmar López 050abd9fab Update pr-review.yml (#16100)
Co-authored-by: Omar López <zomars@me.com>
2024-08-14 11:24:23 -07:00
Peer RichelsenandCarina Wollendorfer 1adf2ad5a8 fix: transcribe UI subtitle were blocking chat (#16097)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-08-14 13:33:17 +00:00
Kartik Saini 3f2d83d89f fix: doesn't fall back to calvideo if wrong destination is used for google meet (#16195) 2024-08-14 15:11:22 +02:00
528a4fbb97 feat: Assign colors to events (#15298)
* feat: Assign colors to events

* update

* final update

* update

* fix requested changes error

* Update EventAdvancedTab.tsx

* add contrast check

* update

* fix contrsterror message visibility

* fix type error

* fix

* update test

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-08-14 15:10:40 +02:00
Joe Au-Yeung 95e2ad3007 fix: Disable all emails per event type w/o a workflow [CAL-4066] (#15902)
* Create confirmation dialog for attendees

* Add disabling host emails option

* Fix DOM issues

* Allow updating disabling emails on backend

* Pass metadata to sendEmails functions

* Clean up

* Type fixes

* Fix test

* Allow for enterprise only

* Type fix

* Test fix

* Check for parentId

* Disable emails for sendScheduledEmails and sendScheduledSeatsEmails

* Remove unused variable

* Only update disable all emails if event type belongs to an org

* Disable scheduling mandatory reminder workflow if attendee emails are disabled

* Refactor disable email UI

* Update copy

* Type fixes

* Fix missing params
2024-08-14 13:17:52 +02:00
c41e5ddc2c feat: add gtm to load in us only (#16146)
* add gtm to load in us only

* fix: Tried to give typescript some help

* fix: Alternative fix

* fix: Add yarn.lock

* fix: Oops

* fix: type err

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-08-14 09:31:01 +00:00
Omar López f206b3d135 feat: e2e reports in gh-pages (#16179) 2024-08-13 21:50:25 +00:00
0b03bcb90e feat: CalendarSettings atom (#16120)
* dumb components for calendar settings

* shadcn switch

* update exports

* update packages

* add calendar settings atom to examples app

* init calendar settings atom

* refactors

* fix import path

* export type for calendar switch props

* invalidate queries on deleting calendar credentials

* replace calendars list with calendar settings wrapper

* cleanup

* update styling

* refactors

* cleanup

* fix: missing key prop CalendarSettingsPlatformWrapper

* Label as client components

* Address client component build errors

* Move QueryCell out of packages/lib

* PR feedback

* more feedback

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2024-08-13 21:51:29 +05:30
Alex van Andelandzomars b09b32b121 fix: scheduleId not unset properly bug (#15993)
* fix: scheduleId not unset properly bug

* Refactor almost complete, couple of tests needed

* More testid fixes

* Add migration to fixup schedule id bug

* Remove migration

* fix: Refactor useLockedFieldManager for correct useState use

---------

Co-authored-by: zomars <zomars@me.com>
2024-08-13 16:44:16 +01:00
Shreya Agarwal d5f7065fdb fix: Replace empty user name with user email (#16176) 2024-08-13 16:29:09 +02:00
dc40cc664d test: Bring back broken out-of-office tests, prevent modal button clickable before flicker, and fix timezone issue (#16001)
* test: prevent modal button clickable before flicker

* chore: add note to migrate to appDir to solve full page flicker issue

* chore: Improve OutOfOfficePage loading experience

Refactor the OutOfOfficePage component to improve the loading experience by using the `isPending` property from the `trpc.viewer.outOfOfficeReasonList.useQuery()` hook. This workaorund eliminates the full page flicker issue and provides a better user experience. The "Add" button now shows a loading state when the query is pending.

* Update index.tsx

* chore: Update outOfOffice.handler.ts to use input.offset instead of calculating offset

* fix e2e

* Update

* revert

* chore: Fix offset calculation for out of office bookings

* chore: Update date range calculation for out-of-office modal

* chore: Remove console.log statement in out-of-office modal

* chore: Update TimeZoneEnum with additional time zones

This commit updates the TimeZoneEnum in the types.ts file to include additional time zones. The new time zones added are:
- ID: Asia/Jakarta (UTC+7)
- SAMOA: Etc/GMT+12 (UTC-12)
- AMERICAN_SAMOA: Pacific/Pago_Pago (UTC-11)
- HAWAII: Pacific/Honolulu (UTC-10)
- ALASKA: America/Anchorage (UTC-9)
- PACIFIC_USA: America/Los_Angeles (UTC-8)
- CENTRAL_USA: America/Chicago (UTC-6)
- EASTERN_USA: America/New_York (UTC-5)
- VENEZUELA: America/Caracas (UTC-4)
- ARGENTINA: America/Buenos_Aires (UTC-3)
- BRAZIL: America/Noronha (UTC-2)
- AZORES: Atlantic/Azores (UTC-1)
- FRANCE: Europe/Paris (UTC+1)
- GREECE: Europe/Athens (UTC+2)
- RUSSIA: Europe/Moscow (UTC+3)
- UAE: Asia/Dubai (UTC+4)
- PAKISTAN: Asia/Karachi (UTC+5)
- BANGLADESH: Asia/Dhaka (UTC+6)
- SINGAPORE: Asia/Singapore (UTC+8)
- JAPAN: Asia/Tokyo (UTC+9)
- AUSTRALIA: Australia/Sydney (UTC+10)
- SOLOMON_ISLANDS: Pacific/Guadalcanal (UTC+11)
- NEW_ZEALAND: Pacific/Auckland (UTC+12)

The purpose of this update is to provide a comprehensive list of time zones for users to choose from when setting their time zone in the out-of-office feature.

* update

* fix

* remove comment

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
2024-08-13 16:21:09 +02:00
Imamuzzaki Abu Salam 3eaf84e961 test: Add E2E tests for orgRedirection=true param (#16066) 2024-08-13 12:32:39 +00:00
Hariom Balhara c36249b5a7 fix: Add booking related events for recurring bookings as well (#16180)
* Add bookingSuccessful event for recurring booking

* Remove unused variable
2024-08-13 10:25:52 +00:00
sean-brydon 3666c260ee fix: remove layout as its already in /future/settings/teams/layout.tsx (#16172) 2024-08-12 16:08:15 +00:00
Peer Richelsen 77f3fd3b38 fixed todesktop nav (#16170) 2024-08-12 14:11:03 +00:00
Anik Dhabal Babu 848bcbfb8f feat: provide option to allow rescheduling with the same round-robin host (#15132)
* feat: provide option to allow rescheduling with the same round-robin
host

* update

* fix type error

* fix and update

* fix type error

* update

* remove

* Update getEventTypesFromDB.ts

* add test

* small fix

* fix requested changes
2024-08-12 15:03:50 +02:00
Hichem Fantarandsean-brydon 24936d8b7b refactor: improve Card component hover effect (#16161)
Simplify the hover effect on the Card component by adding a transition to the transform property. This improves the user experience and makes the hover effect smoother.

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-08-12 11:01:48 +00:00
Natnael YilmaandAmit Sharma ad62f9d2bd fix: reviewed-user-sms-lock (#16016)
* throw trpc error on re-review for sms

* update logic

* test

* update logic

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-08-12 08:12:48 +02:00
Omar López 829dd937d6 fix: add sensible default size for svg icons (#16159) 2024-08-10 02:59:25 +00:00
MorganandPeer Richelsen dda4b17a7c feat: Platform OAuthClient Webhooks (#16134)
* wip

* fixup! Merge branch 'main' into platform-oauth-client-webhooks

* wip

* fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! fixup! fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! fixup! fixup! fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! fixup! fixup! fixup! fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! fixup! fixup! fixup! fixup! fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! Merge branch 'platform-oauth-client-webhooks' of github.com:calcom/cal.com into platform-oauth-client-webhooks

* fixup! fixup! Merge branch 'platform-oauth-client-webhooks' of github.com:calcom/cal.com into platform-oauth-client-webhooks

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-08-09 17:02:39 +02:00
Peer Richelsen 4def31e98f added horizon workrooms (#16152) 2024-08-09 11:02:14 -04:00
Udit TakkarandAlex van Andel 6d3f5495cb revert: fix: #15857 add managed by tooltip (#16139)
* revert: fix: #15857 add managed by tooltip

* chore: remove parent

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-08-09 14:56:06 +01:00
sean-brydon 008af08148 remove requirement for signaturetoken (#16151) 2024-08-09 14:38:05 +01:00
Lauris Skraucis ac0930a11f test: v2 new event-types endpoints handle legacy event-types (#16144)
* test: v2 new event-types endpoints return legacy event-types

* Update event-types.repository.fixture.ts
2024-08-09 13:14:28 +00:00
Udit TakkarandJoe Au-Yeung 0e62800af6 refactor: simplify workflow page and improve load time (#16095)
* fix: simplify workflow page and improve load time

* chore: use new endpoint

* chore: save progress

* refactor: code

* refactor: remove not requried code

* chore: remove schema

* chore: fix typ

* chore: improve

* chore: change name

* chore: remove unused

* chore: remove page

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-08-09 14:48:48 +02:00
sean-brydon b6ab1ef80a fix: broken password inputs (#16148) 2024-08-09 11:24:32 +00:00
Lauris Skraucis 13347edced feat: atoms date overrides modal custom classes (#16147)
* feat: atoms date override modal custom classes

* refactor: rename overridesModalClassName to overridesModalClassNames
2024-08-09 11:05:57 +00:00
d734841184 fix: #15726 increase max recurring events limit input (#15728)
* increased recurring limit input

* updated input max recurring limit to 24

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-08-09 08:24:53 +00:00
a2998b6c84 fix: #15474 retrieve zoom usersettings and update zoom integration (#15489)
* retrieve zoom usersettings and update

* update to filter response

* updated zoomUserSettingsSchema

* abstract to new function, log error

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-08-09 08:19:31 +00:00
Nafees Nazikandzomars da5df7164a perf: use svg sprite for icons (#16135)
Co-authored-by: zomars <zomars@me.com>
2024-08-08 15:12:08 -07:00
3e28160fa7 fix: uncaught PaymentService typings in payment utils (#16107)
* fix: uncaught errors in PaymentService

* fix: uncaught errors in WebhookService

* fix: uncaught errors in sendPayload

* fix: uncaught PaymentService typings in payment utils

* fix: Remove missing users from assignment (#16115)

* remove missing users from assignment

* early return

* fix: use organization default fixedPrice if self-serve/default price is passed in (#15966)

* Use fixed price if default (selfserve) is used

* fix conditional

* use .env instead of hard coded values

* feedback

* fix: display skeleton loader (#16114)

* fix: display loader

* fix: updated skelton loader

* feat: create new markNoShow endpoint (#15738)

* feat: create new markNoShow endpoint

* chore: add schema

* chore: update name

* chore: save progress

* refactor: improve code and types

* fix: unit test

* chore: improvements

* chore: move query to booking repository

* fix: type error

* chore: Remove cal.ai email assistant (#16003)

* Delete apps/ai directory

* removed cal-ai app from app store

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* feat: Add upcoming bookings feature to GET /api/bookings endpoint (#16060)

* feat: Add upcoming bookings feature to GET /api/bookings endpoint

- Added support for filtering upcoming bookings by adding the `upcoming` query parameter to the GET /api/bookings endpoint.
- When `upcoming` is set to "true", only upcoming bookings are returned.
- Regular users can now retrieve only their upcoming bookings by setting `upcoming` to "true".
- System-wide admins and organization admins can also retrieve only upcoming bookings by setting `upcoming` to "true".

* feat: Add status filter to booking API endpoint

This commit adds a new `status` filter to the booking API endpoint. The `status` filter allows filtering bookings by their status, overriding the `dateFrom` and `dateTo` filters. The only valid value for the `status` filter is "upcoming".

* Update _get.ts

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>

* fix: Hide personal booking theme when in an organization (#16117)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* fix: uncaught errors in WebhookService

* fix: uncaught errors in sendPayload

---------

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Imamuzzaki Abu Salam <imamuzzaki@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-08-08 20:15:44 +02:00
Lauris Skraucis 2b9bdb9c80 fix: v2 event-types (#16132)
* refactor: have system and user booking fields schemas and united schema

* fix: event-types output service filter out system booking fields for response

* chore: v2 update platform-libraries to 0.0.25

* chore: platform-libraries export types and update changelog

* chore: add updated lock file
2024-08-08 20:21:56 +03:00
486 changed files with 14164 additions and 10350 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
## Mandatory Tasks (DO NOT REMOVE)
- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com). If N/A, write N/A here and check the checkbox.
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com/docs). If N/A, write N/A here and check the checkbox.
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works.
## How should this be tested?
-2
View File
@@ -37,8 +37,6 @@ runs:
with:
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
key: yarn-download-cache-${{ hashFiles('yarn.lock') }}
restore-keys: |
yarn-download-cache-
# Invalidated on yarn.lock changes
- name: Restore node_modules
@@ -3,6 +3,10 @@ description: "Install playwright, cache and restore if necessary"
runs:
using: "composite"
steps:
- name: Get installed Playwright version
shell: bash
id: playwright-version
run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'])")" >> $GITHUB_ENV
- name: Cache playwright binaries
id: playwright-cache
uses: buildjet/cache@v4
@@ -11,9 +15,7 @@ runs:
~/Library/Caches/ms-playwright
~/.cache/ms-playwright
${{ github.workspace }}/node_modules/playwright
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
restore-keys: cache-playwright-
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
- name: Yarn playwright install
shell: bash
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn playwright install
run: yarn playwright install --with-deps
+58
View File
@@ -0,0 +1,58 @@
name: Delete
on:
delete:
branches-ignore: [main, gh-pages]
# ensures that currently running Playwright workflow of deleted branch gets cancelled
concurrency:
group: ${{ github.event.ref }}
cancel-in-progress: true
jobs:
delete_reports:
name: Delete Reports
runs-on: ubuntu-latest
env:
# Contains all reports for deleted branch
BRANCH_REPORTS_DIR: reports/${{ github.event.ref }}
steps:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v2
with:
ref: gh-pages
- name: Set Git User
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Check for workflow reports
run: |
if [ -z "$(ls -A $BRANCH_REPORTS_DIR)" ]; then
echo "BRANCH_REPORTS_EXIST="false"" >> $GITHUB_ENV
else
echo "BRANCH_REPORTS_EXIST="true"" >> $GITHUB_ENV
fi
- name: Delete reports from repo for branch
if: ${{ env.BRANCH_REPORTS_EXIST == 'true' }}
timeout-minutes: 3
run: |
cd $BRANCH_REPORTS_DIR/..
rm -rf ${{ github.event.ref }}
git add .
git commit -m "workflow: remove all reports for branch ${{ github.event.ref }}"
while true; do
git pull --rebase
if [ $? -ne 0 ]; then
echo "Failed to rebase. Please review manually."
exit 1
fi
git push
if [ $? -eq 0 ]; then
echo "Successfully pushed HTML reports to repo."
exit 0
fi
done
+2 -2
View File
@@ -98,5 +98,5 @@ jobs:
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: app-store-results
path: test-results
name: blob-report-app-store
path: blob-report
+2 -2
View File
@@ -85,5 +85,5 @@ jobs:
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: embed-react-results
path: test-results
name: blob-report-embed-react
path: blob-report
+2 -2
View File
@@ -91,5 +91,5 @@ jobs:
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: embed-core-results
path: test-results
name: blob-report-embed-core
path: blob-report
+4 -3
View File
@@ -89,8 +89,9 @@ jobs:
- name: Run Tests
run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: test-results
name: blob-report-${{ matrix.shard }}
path: blob-report
retention-days: 30
+25
View File
@@ -0,0 +1,25 @@
# https://playwright.dev/docs/test-sharding#merging-reports-from-multiple-shards
on:
workflow_call:
jobs:
merge-reports:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Merge into HTML Report
run: yarn playwright merge-reports --reporter html ./all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: html-report--attempt-${{ github.run_number }}-${{ github.run_attempt }}
path: playwright-report
retention-days: 14
+5 -2
View File
@@ -7,11 +7,14 @@ on:
jobs:
label-pr:
runs-on: ubuntu-latest
permissions:
actions: read
issues: write
pull-requests: write
steps:
- name: Label PR as ready for E2E
if: github.event.review.state == 'approved'
uses: actions-ecosystem/action-add-labels@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
github_token: ${{ secrets.READY_FOR_E2E_PAT }}
labels: 'ready-for-e2e'
+47 -7
View File
@@ -91,44 +91,50 @@ jobs:
console.log('Found the label?', labelFound);
core.setOutput('run-e2e', labelFound);
deps:
name: Install dependencies
needs: [changes, check-label]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/yarn-install.yml
type-check:
name: Type check
needs: [changes, check-label]
needs: [changes, check-label, deps]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/check-types.yml
secrets: inherit
lint:
name: Linters
needs: [changes, check-label]
needs: [changes, check-label, deps]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/lint.yml
secrets: inherit
unit-test:
name: Tests
needs: [changes, check-label]
needs: [changes, check-label, deps]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/unit-tests.yml
secrets: inherit
build-api-v1:
name: Production builds
needs: [changes, check-label]
needs: [changes, check-label, deps]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/api-v1-production-build.yml
secrets: inherit
build-api-v2:
name: Production builds
needs: [changes, check-label]
needs: [changes, check-label, deps]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/api-v2-production-build.yml
secrets: inherit
build:
name: Production builds
needs: [changes, check-label]
needs: [changes, check-label, deps]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/production-build-without-database.yml
secrets: inherit
@@ -181,8 +187,42 @@ jobs:
uses: ./.github/workflows/nextjs-bundle-analysis.yml
secrets: inherit
merge-reports:
name: Merge reports
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
uses: ./.github/workflows/merge-reports.yml
secrets: inherit
publish-report:
name: Publish HTML report
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
permissions:
contents: write
issues: write
pull-requests: write
needs: [changes, check-label, merge-reports]
uses: ./.github/workflows/publish-report.yml
secrets: inherit
required:
needs: [changes, lint, type-check, unit-test, integration-test, check-label, build, build-api-v1, build-api-v2, e2e, e2e-api-v2, e2e-embed, e2e-embed-react, e2e-app-store]
needs:
[
changes,
lint,
type-check,
unit-test,
integration-test,
check-label,
build,
build-api-v1,
build-api-v2,
e2e,
e2e-api-v2,
e2e-embed,
e2e-embed-react,
e2e-app-store,
]
if: always()
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
+96
View File
@@ -0,0 +1,96 @@
on:
workflow_call:
permissions:
contents: write
issues: write
pull-requests: write
jobs:
publish-report:
runs-on: ubuntu-latest
continue-on-error: true
env:
# Unique URL path for each workflow run attempt
HTML_REPORT_URL_PATH: reports/${{ github.head_ref }}/${{ github.run_id }}/${{ github.run_attempt }}
BRANCH_REPORTS_DIR: reports/${{ github.head_ref }}
steps:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v4
with:
ref: gh-pages
- name: Set Git User
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Check for workflow reports
run: |
if [ -z "$(ls -A $BRANCH_REPORTS_DIR)" ]; then
echo "BRANCH_REPORTS_EXIST="false"" >> $GITHUB_ENV
else
echo "BRANCH_REPORTS_EXIST="true"" >> $GITHUB_ENV
fi
- name: Cleanup old HTML reports
if: ${{ env.BRANCH_REPORTS_EXIST == 'true' }}
timeout-minutes: 3
run: |
rm -rf $BRANCH_REPORTS_DIR
git add .
git commit -m "workflow: remove all reports for branch ${{ github.head_ref }}"
- name: Download zipped HTML report
uses: actions/download-artifact@v4
with:
name: html-report--attempt-${{ github.run_number }}-${{ github.run_attempt }}
path: ${{ env.HTML_REPORT_URL_PATH }}
- name: Push HTML Report
timeout-minutes: 3
# commit report, then try push-rebase-loop until it's able to merge the HTML report to the gh-pages branch
# this is necessary when this job running at least twice at the same time (e.g. through two pushes at the same time)
run: |
git add .
git commit -m "workflow: add HTML report for run-id ${{ github.run_id }} (attempt: ${{ github.run_attempt }})"
while true; do
git pull --rebase
if [ $? -ne 0 ]; then
echo "Failed to rebase. Please review manually."
exit 1
fi
git push
if [ $? -eq 0 ]; then
echo "Successfully pushed HTML report to repo."
exit 0
fi
done
- name: Output Report URL as Workflow Annotation
id: url
run: |
FULL_HTML_REPORT_URL=https://calcom.github.io/cal.com/$HTML_REPORT_URL_PATH
echo "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
- name: Find Comment
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: <!-- GENERATED-E2E-RESULTS -->
- name: Create comment
if: steps.fc.outputs.comment-id == ''
uses: peter-evans/create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
<!-- GENERATED-E2E-RESULTS -->
## E2E results are ready!
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
- name: Update comment
if: steps.fc.outputs.comment-id != ''
uses: peter-evans/create-or-update-comment@v3
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
edit-mode: replace
body: |
<!-- GENERATED-E2E-RESULTS -->
## E2E results are ready!
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
+1
View File
@@ -15,3 +15,4 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
+1 -1
View File
@@ -380,7 +380,7 @@ Currently Vercel Pro Plan is required to be able to Deploy this application with
### Elestio
[![Deploy on Elestio](https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png)](https://elest.io/open-source/cal.com)
[![Deploy on Elestio](https://elest.io/images/logos/deploy-to-elestio-btn.png)](https://elest.io/open-source/cal.com)
<!-- ROADMAP -->
+10 -3
View File
@@ -1,5 +1,12 @@
import { _PaymentModel as Payment } from "@calcom/prisma/zod";
// FIXME: Payment seems a delicate endpoint, do we need to remove anything here?
export const schemaPaymentBodyParams = Payment.omit({ id: true });
export const schemaPaymentPublic = Payment.omit({ externalId: true });
export const schemaPaymentPublic = Payment.pick({
id: true,
amount: true,
success: true,
refunded: true,
fee: true,
paymentOption: true,
currency: true,
bookingId: true,
});
+1
View File
@@ -9,6 +9,7 @@ export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true })
isOrganization: true,
isPlatform: true,
smsLockState: true,
smsLockReviewedByAdmin: true,
});
const schemaTeamRequiredParams = z.object({
@@ -27,7 +27,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* tags:
* - availabilities
* externalDocs:
* url: https://docs.cal.com/availability
* url: https://docs.cal.com/docs/core-features/availability
* responses:
* 201:
* description: OK, availability removed successfully
@@ -28,7 +28,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* tags:
* - availabilities
* externalDocs:
* url: https://docs.cal.com/availability
* url: https://docs.cal.com/docs/core-features/availability
* responses:
* 200:
* description: OK
@@ -63,7 +63,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* tags:
* - availabilities
* externalDocs:
* url: https://docs.cal.com/availability
* url: https://docs.cal.com/docs/core-features/availability
* responses:
* 201:
* description: OK, availability edited successfully
@@ -62,7 +62,7 @@ import {
* tags:
* - availabilities
* externalDocs:
* url: https://docs.cal.com/availability
* url: https://docs.cal.com/docs/core-features/availability
* responses:
* 201:
* description: OK, availability created
+2 -2
View File
@@ -11,8 +11,8 @@ const swaggerHandler = withSwagger({
{ url: "https://api.cal.com/v1" },
],
externalDocs: {
url: "https://docs.cal.com",
description: "Find more info at our main docs: https://docs.cal.com/",
url: "https://docs.cal.com/docs",
description: "Find more info at our main docs: https://docs.cal.com/docs/",
},
info: {
title: `${pjson.name}: ${pjson.description}`,
@@ -28,7 +28,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* tags:
* - event-types
* externalDocs:
* url: https://docs.cal.com/core-features/event-types
* url: https://docs.cal.com/docs/core-features/event-types
* responses:
* 201:
* description: OK, eventType removed successfully
@@ -34,7 +34,7 @@ import getCalLink from "../_utils/getCalLink";
* tags:
* - event-types
* externalDocs:
* url: https://docs.cal.com/core-features/event-types
* url: https://docs.cal.com/docs/core-features/event-types
* responses:
* 200:
* description: OK
@@ -194,7 +194,7 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
* tags:
* - event-types
* externalDocs:
* url: https://docs.cal.com/core-features/event-types
* url: https://docs.cal.com/docs/core-features/event-types
* responses:
* 201:
* description: OK, eventType edited successfully
+1 -1
View File
@@ -33,7 +33,7 @@ import getCalLink from "./_utils/getCalLink";
* tags:
* - event-types
* externalDocs:
* url: https://docs.cal.com/core-features/event-types
* url: https://docs.cal.com/docs/core-features/event-types
* responses:
* 200:
* description: OK
+1 -1
View File
@@ -255,7 +255,7 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
* tags:
* - event-types
* externalDocs:
* url: https://docs.cal.com/core-features/event-types
* url: https://docs.cal.com/docs/core-features/event-types
* responses:
* 201:
* description: OK, event type created
@@ -125,6 +125,7 @@ export async function patchHandler(req: NextApiRequest) {
metadata: NonNullable<typeof data.metadata> | undefined;
} = {
...data,
smsLockReviewedByAdmin: false,
metadata: data.metadata === null ? {} : data.metadata || undefined,
};
const team = await prisma.team.update({ where: { id: teamId }, data: cloneData });
@@ -32,7 +32,7 @@ const querySchema = z.object({
* tags:
* - event-types
* externalDocs:
* url: https://docs.cal.com/core-features/event-types
* url: https://docs.cal.com/docs/core-features/event-types
* responses:
* 200:
* description: OK
+1
View File
@@ -125,6 +125,7 @@ async function postHandler(req: NextApiRequest) {
metadata: NonNullable<typeof data.metadata> | undefined;
} = {
...data,
smsLockReviewedByAdmin: false,
metadata: data.metadata === null ? {} : data.metadata || undefined,
};
@@ -27,7 +27,7 @@ import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
* tags:
* - webhooks
* externalDocs:
* url: https://docs.cal.com/core-features/webhooks
* url: https://docs.cal.com/docs/core-features/webhooks
* responses:
* 201:
* description: OK, hook removed successfully
+1 -1
View File
@@ -28,7 +28,7 @@ import { schemaWebhookReadPublic } from "~/lib/validations/webhook";
* tags:
* - webhooks
* externalDocs:
* url: https://docs.cal.com/core-features/webhooks
* url: https://docs.cal.com/docs/core-features/webhooks
* responses:
* 200:
* description: OK
@@ -58,7 +58,7 @@ import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "~/lib/vali
* tags:
* - webhooks
* externalDocs:
* url: https://docs.cal.com/core-features/webhooks
* url: https://docs.cal.com/docs/core-features/webhooks
* responses:
* 201:
* description: OK, webhook edited successfully
+1 -1
View File
@@ -24,7 +24,7 @@ import { schemaWebhookReadPublic } from "~/lib/validations/webhook";
* tags:
* - webhooks
* externalDocs:
* url: https://docs.cal.com/core-features/webhooks
* url: https://docs.cal.com/docs/core-features/webhooks
* responses:
* 200:
* description: OK
+1 -1
View File
@@ -56,7 +56,7 @@ import { schemaWebhookCreateBodyParams, schemaWebhookReadPublic } from "~/lib/va
* tags:
* - webhooks
* externalDocs:
* url: https://docs.cal.com/core-features/webhooks
* url: https://docs.cal.com/docs/core-features/webhooks
* responses:
* 201:
* description: OK, webhook created
+1 -1
View File
@@ -25,8 +25,8 @@
},
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.28",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-libraries-0.0.23": "npm:@calcom/platform-libraries@0.0.23",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
@@ -20,7 +20,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { handleNewBooking } from "@calcom/platform-libraries-0.0.23";
import { handleNewBooking } from "@calcom/platform-libraries";
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
describe("Bookings Endpoints", () => {
@@ -46,7 +46,7 @@ import {
getBookingInfo,
handleCancelBooking,
getBookingForReschedule,
} from "@calcom/platform-libraries-0.0.23";
} from "@calcom/platform-libraries";
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
import { ApiResponse } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
@@ -1,7 +1,7 @@
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
import type { AppsStatus } from "@calcom/platform-libraries-0.0.23";
import type { AppsStatus } from "@calcom/platform-libraries";
export class CreateRecurringBookingInput extends CreateBookingInput {
@IsBoolean()
@@ -105,7 +105,7 @@ class Primary {
credentialId!: number;
}
class Calendar {
export class Calendar {
@IsEmail()
externalId!: string;
@@ -135,7 +135,7 @@ class Calendar {
credentialId!: number;
}
class ConnectedCalendar {
export class ConnectedCalendar {
@ValidateNested()
@IsObject()
integration!: Integration;
@@ -5,7 +5,7 @@ import { BadRequestException, UnauthorizedException } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { SUCCESS_STATUS, APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries-0.0.23";
import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries";
@Injectable()
export class AppleCalendarService implements CredentialSyncCalendarApp {
@@ -18,7 +18,7 @@ import { User } from "@prisma/client";
import { DateTime } from "luxon";
import { z } from "zod";
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries-0.0.23";
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries";
import { Calendar } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
@@ -35,7 +35,7 @@ import {
EventTypesPublic,
eventTypeBookingFields,
eventTypeLocations,
} from "@calcom/platform-libraries-0.0.23";
} from "@calcom/platform-libraries";
import { ApiSuccessResponse } from "@calcom/platform-types";
describe("Event types Endpoints", () => {
@@ -4,7 +4,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { getEventTypeById } from "@calcom/platform-libraries-0.0.23";
import { getEventTypeById } from "@calcom/platform-libraries";
import type { PrismaClient } from "@calcom/prisma";
@Injectable()
@@ -177,6 +177,10 @@ export class BookingField_2024_04_15 {
@ValidateNested({ each: true })
@Type(() => Source)
sources?: Source[];
@IsOptional()
@IsBoolean()
disableOnPrefill?: boolean;
}
export class RecurringEvent_2024_04_15 {
@@ -87,6 +87,10 @@ class BookingField {
@Type(() => Source)
@IsOptional()
sources?: Source[];
@IsOptional()
@IsBoolean()
disableOnPrefill?: boolean;
}
class Organization {
@@ -14,7 +14,7 @@ import {
updateEventType,
EventTypesPublic,
getEventTypesPublic,
} from "@calcom/platform-libraries-0.0.23";
} from "@calcom/platform-libraries";
import { EventType } from "@calcom/prisma/client";
@Injectable()
@@ -1,5 +1,6 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
@@ -25,6 +26,8 @@ import {
EventTypeOutput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-types/dist/event-types/event-types_2024_06_14/inputs/enums/booking-window.enum";
import { SchedulingType } from "@calcom/prisma/enums";
describe("Event types Endpoints", () => {
describe("Not authenticated", () => {
@@ -212,6 +215,21 @@ describe("Event types Endpoints", () => {
},
],
scheduleId: firstSchedule.id,
bookingLimitsCount: {
day: 2,
week: 5,
},
onlyShowFirstAvailableSlot: true,
bookingLimitsDuration: {
day: 60,
week: 100,
},
offsetStart: 30,
bookingWindow: {
type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays,
value: 30,
rolling: true,
},
};
return request(app.getHttpServer())
@@ -230,7 +248,11 @@ describe("Event types Endpoints", () => {
expect(createdEventType.bookingFields).toEqual(body.bookingFields);
expect(createdEventType.ownerId).toEqual(user.id);
expect(createdEventType.scheduleId).toEqual(firstSchedule.id);
expect(createdEventType.bookingLimitsCount).toEqual(body.bookingLimitsCount);
expect(createdEventType.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
expect(createdEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
expect(createdEventType.offsetStart).toEqual(body.offsetStart);
expect(createdEventType.bookingWindow).toEqual(body.bookingWindow);
eventType = responseBody.data;
});
});
@@ -241,6 +263,21 @@ describe("Event types Endpoints", () => {
const body: UpdateEventTypeInput_2024_06_14 = {
title: newTitle,
scheduleId: secondSchedule.id,
bookingLimitsCount: {
day: 4,
week: 10,
},
onlyShowFirstAvailableSlot: true,
bookingLimitsDuration: {
day: 100,
week: 200,
},
offsetStart: 50,
bookingWindow: {
type: BookingWindowPeriodInputTypeEnum_2024_06_14.businessDays,
value: 40,
rolling: false,
},
};
return request(app.getHttpServer())
@@ -261,9 +298,19 @@ describe("Event types Endpoints", () => {
expect(updatedEventType.bookingFields).toEqual(eventType.bookingFields);
expect(updatedEventType.ownerId).toEqual(user.id);
expect(updatedEventType.scheduleId).toEqual(secondSchedule.id);
expect(updatedEventType.bookingLimitsCount).toEqual(body.bookingLimitsCount);
expect(updatedEventType.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
expect(updatedEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
expect(updatedEventType.offsetStart).toEqual(body.offsetStart);
expect(updatedEventType.bookingWindow).toEqual(body.bookingWindow);
eventType.title = newTitle;
eventType.scheduleId = secondSchedule.id;
eventType.bookingLimitsCount = updatedEventType.bookingLimitsCount;
eventType.onlyShowFirstAvailableSlot = updatedEventType.onlyShowFirstAvailableSlot;
eventType.bookingLimitsDuration = updatedEventType.bookingLimitsDuration;
eventType.offsetStart = updatedEventType.offsetStart;
eventType.bookingWindow = updatedEventType.bookingWindow;
});
});
@@ -299,6 +346,11 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType.locations).toEqual(eventType.locations);
expect(fetchedEventType.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType.ownerId).toEqual(user.id);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
});
it(`/GET/even-types by username`, async () => {
@@ -324,6 +376,11 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
});
it(`/GET/event-types by username and eventSlug`, async () => {
@@ -344,6 +401,11 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
});
it(`/GET/:id not existing`, async () => {
@@ -380,4 +442,329 @@ describe("Event types Endpoints", () => {
await app.close();
});
});
describe("Handle legacy event-types booking fields", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
const userEmail = "legacy-event-types-test-e2e@api.com";
const name = "bob-the-builder";
const username = name;
let user: User;
let legacyEventTypeId1: number;
let legacyEventTypeId2: number;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
})
)
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization" });
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
name,
username,
});
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["redirect-uri"],
permissions: 32,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it("should be defined", () => {
expect(oauthClientRepositoryFixture).toBeDefined();
expect(userRepositoryFixture).toBeDefined();
expect(oAuthClient).toBeDefined();
expect(user).toBeDefined();
});
it("should not allow creating an event type with input of event-types version 2024_04_15", async () => {
const body: CreateEventTypeInput_2024_04_15 = {
title: "Coding class",
slug: "coding-class",
description: "Let's learn how to code like a pro.",
length: 60,
locations: [{ type: "integrations:daily" }],
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
});
it("should return empty bookingFields if system fields are the only one in database", async () => {
const legacyEventTypeInput = {
title: "legacy event type",
description: "legacy event type description",
length: 40,
hidden: false,
slug: "legacy-event-type",
locations: [],
schedulingType: SchedulingType.ROUND_ROBIN,
bookingFields: [
{
name: "name",
type: "name",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system",
required: true,
defaultLabel: "your_name",
},
{
name: "email",
type: "email",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system",
required: true,
defaultLabel: "email_address",
},
{
name: "location",
type: "radioInput",
label: "",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system",
required: false,
placeholder: "",
defaultLabel: "location",
getOptionsAt: "locations",
optionsInputs: {
phone: { type: "phone", required: true, placeholder: "" },
attendeeInPerson: { type: "address", required: true, placeholder: "" },
},
hideWhenJustOneOption: true,
},
{
name: "title",
type: "text",
hidden: true,
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: true,
defaultLabel: "what_is_this_meeting_about",
defaultPlaceholder: "",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
{
name: "guests",
type: "multiemail",
hidden: false,
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_guests",
defaultPlaceholder: "email",
},
{
name: "rescheduleReason",
type: "textarea",
views: [{ id: "reschedule", label: "Reschedule View" }],
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "reason_for_reschedule",
defaultPlaceholder: "reschedule_placeholder",
},
],
};
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
legacyEventTypeId1 = legacyEventType.id;
return request(app.getHttpServer())
.get(`/api/v2/event-types/${legacyEventTypeId1}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([]);
});
});
it("should return user created bookingFields among system fields in the database", async () => {
const userDefinedBookingField = {
name: "team",
type: "textarea",
label: "your team",
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: true,
placeholder: "FC Barcelona",
};
const legacyEventTypeInput = {
title: "legacy event type two",
description: "legacy event type description two",
length: 40,
hidden: false,
slug: "legacy-event-type-two",
locations: [],
schedulingType: SchedulingType.ROUND_ROBIN,
bookingFields: [
userDefinedBookingField,
{
name: "name",
type: "name",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system",
required: true,
defaultLabel: "your_name",
},
{
name: "email",
type: "email",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system",
required: true,
defaultLabel: "email_address",
},
{
name: "location",
type: "radioInput",
label: "",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system",
required: false,
placeholder: "",
defaultLabel: "location",
getOptionsAt: "locations",
optionsInputs: {
phone: { type: "phone", required: true, placeholder: "" },
attendeeInPerson: { type: "address", required: true, placeholder: "" },
},
hideWhenJustOneOption: true,
},
{
name: "title",
type: "text",
hidden: true,
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: true,
defaultLabel: "what_is_this_meeting_about",
defaultPlaceholder: "",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
{
name: "guests",
type: "multiemail",
hidden: false,
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_guests",
defaultPlaceholder: "email",
},
{
name: "rescheduleReason",
type: "textarea",
views: [{ id: "reschedule", label: "Reschedule View" }],
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "reason_for_reschedule",
defaultPlaceholder: "reschedule_placeholder",
},
],
};
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
legacyEventTypeId2 = legacyEventType.id;
return request(app.getHttpServer())
.get(`/api/v2/event-types/${legacyEventTypeId2}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([
{
type: userDefinedBookingField.type,
slug: userDefinedBookingField.name,
label: userDefinedBookingField.label,
required: userDefinedBookingField.required,
placeholder: userDefinedBookingField.placeholder,
},
]);
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
try {
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
} catch (e) {
// Event type might have been deleted by the test
}
try {
await userRepositoryFixture.delete(user.id);
} catch (e) {
// User might have been deleted by the test
}
await app.close();
});
});
});
@@ -7,13 +7,24 @@ import {
getEventTypeById,
transformApiEventTypeBookingFields,
transformApiEventTypeLocations,
} from "@calcom/platform-libraries-0.0.23";
} from "@calcom/platform-libraries";
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import type { PrismaClient } from "@calcom/prisma";
type InputEventTransformed = Omit<
CreateEventTypeInput_2024_06_14,
"lengthInMinutes" | "locations" | "bookingFields"
| "lengthInMinutes"
| "locations"
| "bookingFields"
| "bookingLimitsCount"
| "onlyShowFirstAvailableSlot"
| "bookingLimitsDuration"
| "offsetStart"
| "periodType"
| "periodDays"
| "periodCountCalendarDays"
| "periodStartDate"
| "periodEndDate"
> & {
length: number;
slug: string;
@@ -10,9 +10,9 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.23";
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries-0.0.23";
import { dynamicEvent } from "@calcom/platform-libraries-0.0.23";
import { createEventType, updateEventType } from "@calcom/platform-libraries";
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
import { dynamicEvent } from "@calcom/platform-libraries";
import {
CreateEventTypeInput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
@@ -38,7 +38,16 @@ export class EventTypesService_2024_06_14 {
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput_2024_06_14) {
await this.checkCanCreateEventType(user.id, body);
const eventTypeUser = await this.getUserToCreateEvent(user);
const bodyTransformed = this.inputEventTypesService.transformInputCreateEventType(body);
const {
bookingLimits,
durationLimits,
periodType = undefined,
periodDays = undefined,
periodCountCalendarDays = undefined,
periodStartDate = undefined,
periodEndDate = undefined,
...bodyTransformed
} = this.inputEventTypesService.transformInputCreateEventType(body);
const { eventType: eventTypeCreated } = await createEventType({
input: bodyTransformed,
ctx: {
@@ -49,6 +58,26 @@ export class EventTypesService_2024_06_14 {
},
});
await updateEventType({
input: {
id: eventTypeCreated.id,
bookingLimits,
durationLimits,
periodType,
periodDays,
periodCountCalendarDays,
periodStartDate,
periodEndDate,
...bodyTransformed,
},
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prisma: this.dbWrite.prisma,
},
});
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeCreated.id);
if (!eventType) {
@@ -95,13 +124,16 @@ export class EventTypesService_2024_06_14 {
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
: false;
const profileId = user.movedToProfile?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
return {
id: user.id,
role: user.role,
username: user.username,
organizationId: user.organizationId,
organization: { isOrgAdmin },
profile: { id: profileId },
metadata: user.metadata,
selectedCalendars,
};
}
@@ -3,7 +3,9 @@ import { Injectable } from "@nestjs/common";
import {
transformApiEventTypeBookingFields,
transformApiEventTypeLocations,
} from "@calcom/platform-libraries-0.0.23";
transformApiEventTypeIntervalLimits,
transformApiEventTypeFutureBookingLimits,
} from "@calcom/platform-libraries";
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
@Injectable()
@@ -16,20 +18,42 @@ export class InputEventTypesService_2024_06_14 {
},
];
const { lengthInMinutes, locations, bookingFields, ...rest } = inputEventType;
const {
lengthInMinutes,
locations,
bookingFields,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
...rest
} = inputEventType;
const eventType = {
...rest,
length: lengthInMinutes,
locations: this.transformInputLocations(locations || defaultLocations),
bookingFields: this.transformInputBookingFields(bookingFields),
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
: undefined,
...this.transformInputBookingWindow(bookingWindow),
};
return eventType;
}
transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14) {
const { lengthInMinutes, locations, bookingFields, scheduleId, ...rest } = inputEventType;
const {
lengthInMinutes,
locations,
bookingFields,
scheduleId,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
...rest
} = inputEventType;
const eventType = {
...rest,
@@ -37,6 +61,11 @@ export class InputEventTypesService_2024_06_14 {
locations: locations ? this.transformInputLocations(locations) : undefined,
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
schedule: scheduleId,
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
: undefined,
...this.transformInputBookingWindow(bookingWindow),
};
return eventType;
@@ -49,4 +78,13 @@ export class InputEventTypesService_2024_06_14 {
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
return transformApiEventTypeBookingFields(inputBookingFields);
}
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
return transformApiEventTypeIntervalLimits(inputBookingFields);
}
transformInputBookingWindow(inputBookingWindow: CreateEventTypeInput_2024_06_14["bookingWindow"]) {
const res = transformApiEventTypeFutureBookingLimits(inputBookingWindow);
return !!res ? res : {};
}
}
@@ -9,7 +9,13 @@ import {
parseRecurringEvent,
TransformedLocationsSchema,
BookingFieldsSchema,
} from "@calcom/platform-libraries-0.0.23";
SystemField,
UserField,
parseBookingLimit,
getResponseEventTypeIntervalLimits,
getResponseEventTypeFutureBookingLimits,
} from "@calcom/platform-libraries";
import { TransformFutureBookingsLimitSchema_2024_06_14 } from "@calcom/platform-types";
type EventTypeRelations = { users: User[]; schedule: Schedule | null };
type DatabaseEventType = EventType & EventTypeRelations;
@@ -42,6 +48,15 @@ type Input = Pick<
| "metadata"
| "users"
| "scheduleId"
| "bookingLimits"
| "durationLimits"
| "onlyShowFirstAvailableSlot"
| "offsetStart"
| "periodType"
| "periodDays"
| "periodCountCalendarDays"
| "periodStartDate"
| "periodEndDate"
>;
@Injectable()
@@ -69,13 +84,26 @@ export class OutputEventTypesService_2024_06_14 {
seatsShowAvailabilityCount,
isInstantEvent,
scheduleId,
onlyShowFirstAvailableSlot,
offsetStart,
} = databaseEventType;
const locations = this.transformLocations(databaseEventType.locations);
const bookingFields = this.transformBookingFields(databaseEventType.bookingFields);
const bookingFields = databaseEventType.bookingFields
? this.transformBookingFields(BookingFieldsSchema.parse(databaseEventType.bookingFields))
: [];
const recurringEvent = this.transformRecurringEvent(databaseEventType.recurringEvent);
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
const users = this.transformUsers(databaseEventType.users);
const bookingLimitsCount = this.transformIntervalLimits(databaseEventType.bookingLimits);
const bookingLimitsDuration = this.transformIntervalLimits(databaseEventType.durationLimits);
const bookingWindow = this.transformBookingWindow({
periodType: databaseEventType.periodType,
periodDays: databaseEventType.periodDays,
periodCountCalendarDays: databaseEventType.periodCountCalendarDays,
periodStartDate: databaseEventType.periodStartDate,
periodEndDate: databaseEventType.periodEndDate,
} as TransformFutureBookingsLimitSchema_2024_06_14);
return {
id,
@@ -105,6 +133,11 @@ export class OutputEventTypesService_2024_06_14 {
isInstantEvent,
users,
scheduleId,
bookingLimitsCount,
bookingLimitsDuration,
onlyShowFirstAvailableSlot,
offsetStart,
bookingWindow,
};
}
@@ -113,9 +146,10 @@ export class OutputEventTypesService_2024_06_14 {
return getResponseEventTypeLocations(TransformedLocationsSchema.parse(locations));
}
transformBookingFields(inputBookingFields: any) {
transformBookingFields(inputBookingFields: (SystemField | UserField)[] | null) {
if (!inputBookingFields) return [];
return getResponseEventTypeBookingFields(BookingFieldsSchema.parse(inputBookingFields));
const userFields = inputBookingFields.filter((field) => field.editable === "user") as UserField[];
return getResponseEventTypeBookingFields(userFields);
}
transformRecurringEvent(recurringEvent: any) {
@@ -143,4 +177,13 @@ export class OutputEventTypesService_2024_06_14 {
};
});
}
transformIntervalLimits(bookingLimits: any) {
const bookingLimitsParsed = parseBookingLimit(bookingLimits);
return getResponseEventTypeIntervalLimits(bookingLimitsParsed);
}
transformBookingWindow(bookingLimits: TransformFutureBookingsLimitSchema_2024_06_14) {
return getResponseEventTypeFutureBookingLimits(bookingLimits);
}
}
@@ -9,8 +9,10 @@ import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import { User, Team } from "@prisma/client";
import * as request from "supertest";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
@@ -25,9 +27,11 @@ describe("Me Endpoints", () => {
let userRepositoryFixture: UserRepositoryFixture;
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
const userEmail = "me-controller-e2e@api.com";
let user: User;
let org: Team;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -43,6 +47,9 @@ describe("Me Endpoints", () => {
.compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
@@ -50,6 +57,20 @@ describe("Me Endpoints", () => {
username: userEmail,
});
org = await organizationsRepositoryFixture.create({
name: "Test org team",
isOrganization: true,
isPlatform: true,
});
await profilesRepositoryFixture.create({
uid: "asd-asd",
username: userEmail,
user: { connect: { id: user.id } },
organization: { connect: { id: org.id } },
movedFromUser: { connect: { id: user.id } },
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
@@ -75,6 +96,8 @@ describe("Me Endpoints", () => {
expect(responseBody.data.defaultScheduleId).toEqual(user.defaultScheduleId);
expect(responseBody.data.weekStart).toEqual(user.weekStart);
expect(responseBody.data.timeZone).toEqual(user.timeZone);
expect(responseBody.data.organization?.isPlatform).toEqual(true);
expect(responseBody.data.organization?.id).toEqual(org.id);
});
});
@@ -138,6 +161,7 @@ describe("Me Endpoints", () => {
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await organizationsRepositoryFixture.delete(org.id);
await app.close();
});
});
+16 -3
View File
@@ -7,6 +7,7 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
import { Controller, UseGuards, Get, Patch, Body } from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
@@ -23,14 +24,26 @@ import { userSchemaResponse } from "@calcom/platform-types";
export class MeController {
constructor(
private readonly usersRepository: UsersRepository,
private readonly schedulesService: SchedulesService_2024_04_15
private readonly schedulesService: SchedulesService_2024_04_15,
private readonly usersService: UsersService
) {}
@Get("/")
@Permissions([PROFILE_READ])
async getMe(@GetUser() user: UserWithProfile): Promise<GetMeOutput> {
const me = userSchemaResponse.parse(user);
const organization = this.usersService.getUserMainProfile(user)?.organization;
const me = userSchemaResponse.parse(
organization
? {
...user,
organizationId: organization.id,
organization: {
id: organization.id,
isPlatform: organization.isPlatform,
},
}
: user
);
return {
status: SUCCESS_STATUS,
data: me,
+12 -1
View File
@@ -1,5 +1,11 @@
import { IsInt, IsEmail, IsOptional, IsString } from "class-validator";
import { Type } from "class-transformer";
import { IsInt, IsEmail, IsOptional, IsString, ValidateNested } from "class-validator";
export class MeOrgOutput {
isPlatform!: boolean;
id!: number;
}
export class MeOutput {
@IsInt()
id!: number;
@@ -25,4 +31,9 @@ export class MeOutput {
@IsInt()
organizationId!: number | null;
@IsOptional()
@ValidateNested()
@Type(() => MeOrgOutput)
organization?: MeOrgOutput;
}
@@ -1,9 +1,6 @@
import { Injectable } from "@nestjs/common";
import {
transformApiScheduleOverrides,
transformApiScheduleAvailability,
} from "@calcom/platform-libraries-0.0.23";
import { transformApiScheduleOverrides, transformApiScheduleAvailability } from "@calcom/platform-libraries";
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
import { ScheduleOverrideInput_2024_06_11 } from "@calcom/platform-types";
@@ -2,7 +2,7 @@ import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
import { Request } from "express";
import { ERROR_STATUS } from "@calcom/platform-constants";
import { TRPCError } from "@calcom/platform-libraries-0.0.23";
import { TRPCError } from "@calcom/platform-libraries";
import { Response } from "@calcom/platform-types";
@Catch(TRPCError)
@@ -4,6 +4,7 @@ import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Reflector } from "@nestjs/core";
import { getToken } from "next-auth/jwt";
import { hasPermissions } from "@calcom/platform-utils";
@@ -24,6 +25,12 @@ export class PermissionsGuard implements CanActivate {
const request = context.switchToHttp().getRequest();
const authString = request.get("Authorization")?.replace("Bearer ", "");
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
if (nextAuthToken) {
return true;
}
if (!authString) {
return false;
@@ -11,6 +11,7 @@ import { Injectable, InternalServerErrorException, UnauthorizedException } from
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import type { Request } from "express";
import { getToken } from "next-auth/jwt";
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
@@ -45,6 +46,13 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
return await this.authenticateBearerToken(bearerToken, requestOrigin);
}
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
if (nextAuthToken) {
return await this.authenticateNextAuth(nextAuthToken);
}
throw new UnauthorizedException(
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers"
);
@@ -58,6 +66,11 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
}
}
async authenticateNextAuth(token: { email?: string | null }) {
const user = await this.nextAuthStrategy(token);
return this.success(user);
}
async authenticateOAuthClient(oAuthClientId: string, oAuthClientSecret: string) {
const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret);
return this.success(user);
@@ -163,4 +176,17 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(ownerId);
return user;
}
async nextAuthStrategy(token: { email?: string | null }) {
if (!token.email) {
throw new UnauthorizedException("Email not found in the authentication token.");
}
const user = await this.userRepository.findByEmailWithProfile(token.email);
if (!user) {
throw new UnauthorizedException("User associated with the authentication token email not found.");
}
return user;
}
}
@@ -0,0 +1,171 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { DestinationCalendarsOutputResponseDto } from "@/modules/destination-calendars/outputs/destination-calendars.output";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User, Credential } from "@prisma/client";
import * as request from "supertest";
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
const CLIENT_REDIRECT_URI = "http://localhost:5555";
describe("Platform Destination Calendar Endpoints", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let tokensRepositoryFixture: TokensRepositoryFixture;
let credentialsRepositoryFixture: CredentialsRepositoryFixture;
let appleCalendarCredentials: Credential;
let user: User;
let accessTokenSecret: string;
let refreshTokenSecret: string;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, TokensModule],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization" });
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.createOAuthManagedUser("office365-connect@gmail.com", oAuthClient.id);
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClient.id);
accessTokenSecret = tokens.accessToken;
refreshTokenSecret = tokens.refreshToken;
appleCalendarCredentials = await credentialsRepositoryFixture.create(
APPLE_CALENDAR_TYPE,
{},
user.id,
APPLE_CALENDAR_ID
);
jest.spyOn(CalendarsService.prototype, "getCalendars").mockReturnValue(
Promise.resolve({
connectedCalendars: [
{
integration: {
installed: false,
type: "apple_calendar",
title: "",
name: "",
description: "",
variant: "calendar",
slug: "",
locationOption: null,
categories: ["calendar"],
logo: "",
publisher: "",
url: "",
email: "",
},
calendars: {
externalId:
"https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
readOnly: false,
integration: "apple_calendar",
credentialId: appleCalendarCredentials.id,
primary: true,
email: user.email,
},
error: { message: "" },
},
],
})
);
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: [CLIENT_REDIRECT_URI],
permissions: 32,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it("should be defined", () => {
expect(oauthClientRepositoryFixture).toBeDefined();
expect(userRepositoryFixture).toBeDefined();
expect(oAuthClient).toBeDefined();
expect(accessTokenSecret).toBeDefined();
expect(refreshTokenSecret).toBeDefined();
expect(user).toBeDefined();
});
it(`POST /v2/destination-calendars: it should respond with a 200 returning back the user updated destination calendar`, async () => {
const body = {
integration: appleCalendarCredentials.type,
externalId: "https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
};
return request(app.getHttpServer())
.put("/v2/destination-calendars")
.set("Authorization", `Bearer ${accessTokenSecret}`)
.send(body)
.expect(200)
.then(async (response) => {
const responseBody: DestinationCalendarsOutputResponseDto = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.credentialId).toEqual(appleCalendarCredentials.id);
expect(responseBody.data.integration).toEqual(body.integration);
expect(responseBody.data.externalId).toEqual(body.externalId);
expect(responseBody.data.userId).toEqual(user.id);
});
});
it(`POST /v2/destination-calendars: should fail 400 if calendar type is invalid`, async () => {
const body = {
integration: "not-supported-calendar",
externalId: "https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
};
return request(app.getHttpServer())
.put("/v2/destination-calendars")
.set("Authorization", `Bearer ${accessTokenSecret}`)
.send(body)
.expect(400);
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(user.email);
await app.close();
});
});
@@ -0,0 +1,46 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { DestinationCalendarsInputBodyDto } from "@/modules/destination-calendars/inputs/destination-calendars.input";
import {
DestinationCalendarsOutputDto,
DestinationCalendarsOutputResponseDto,
} from "@/modules/destination-calendars/outputs/destination-calendars.output";
import { DestinationCalendarsService } from "@/modules/destination-calendars/services/destination-calendars.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Body, Controller, Put, UseGuards } from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@Controller({
path: "/v2/destination-calendars",
version: API_VERSIONS_VALUES,
})
@DocsTags("Destination-Calendars")
export class DestinationCalendarsController {
constructor(private readonly destinationCalendarsService: DestinationCalendarsService) {}
@Put("/")
@UseGuards(ApiAuthGuard)
@DocsTags("Select a third party destination calendar where events will be created")
async updateDestinationCalendars(
@Body() input: DestinationCalendarsInputBodyDto,
@GetUser() user: UserWithProfile
): Promise<DestinationCalendarsOutputResponseDto> {
const { integration, externalId } = input;
const updatedDestinationCalendar = await this.destinationCalendarsService.updateDestinationCalendars(
integration,
externalId,
user.id
);
return {
status: SUCCESS_STATUS,
data: plainToClass(DestinationCalendarsOutputDto, updatedDestinationCalendar, {
strategy: "excludeAll",
}),
};
}
}
@@ -0,0 +1,26 @@
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { DestinationCalendarsController } from "@/modules/destination-calendars/controllers/destination-calendars.controller";
import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository";
import { DestinationCalendarsService } from "@/modules/destination-calendars/services/destination-calendars.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [
CalendarsRepository,
CalendarsService,
DestinationCalendarsService,
DestinationCalendarsRepository,
UsersRepository,
CredentialsRepository,
AppsRepository,
],
controllers: [DestinationCalendarsController],
exports: [DestinationCalendarsRepository],
})
export class DestinationCalendarsModule {}
@@ -0,0 +1,34 @@
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class DestinationCalendarsRepository {
constructor(private readonly dbWrite: PrismaWriteService) {}
async updateCalendar(
integration: string,
externalId: string,
credentialId: number,
userId: number,
primaryEmail: string | null
) {
return await this.dbWrite.prisma.destinationCalendar.upsert({
update: {
integration,
externalId,
credentialId,
primaryEmail,
},
create: {
integration,
externalId,
credentialId,
primaryEmail,
userId,
},
where: {
userId: userId,
},
});
}
}
@@ -0,0 +1,36 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose } from "class-transformer";
import { IsString, IsEnum } from "class-validator";
import {
APPLE_CALENDAR_TYPE,
GOOGLE_CALENDAR_TYPE,
OFFICE_365_CALENDAR_TYPE,
} from "@calcom/platform-constants";
export class DestinationCalendarsInputBodyDto {
@IsString()
@Expose()
@ApiProperty({
example: APPLE_CALENDAR_TYPE,
description: "The calendar service you want to integrate, as returned by the /calendars endpoint",
enum: [APPLE_CALENDAR_TYPE, GOOGLE_CALENDAR_TYPE, OFFICE_365_CALENDAR_TYPE],
required: true,
})
@IsEnum([APPLE_CALENDAR_TYPE, GOOGLE_CALENDAR_TYPE, OFFICE_365_CALENDAR_TYPE])
readonly integration!:
| typeof APPLE_CALENDAR_TYPE
| typeof GOOGLE_CALENDAR_TYPE
| typeof OFFICE_365_CALENDAR_TYPE;
@IsString()
@Expose()
@ApiProperty({
example: "https://caldav.icloud.com/26962146906/calendars/1644422A-1945-4438-BBC0-4F0Q23A57R7S/",
description:
"Unique identifier used to represent the specfic calendar, as returned by the /calendars endpoint",
type: "string",
required: true,
})
readonly externalId!: string;
}
@@ -0,0 +1,34 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsInt, IsString, ValidateNested, IsEnum } from "class-validator";
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
export class DestinationCalendarsOutputDto {
@IsInt()
@Expose()
readonly userId!: number;
@IsString()
@Expose()
readonly integration!: string;
@IsString()
@Expose()
readonly externalId!: string;
@IsInt()
@Expose()
readonly credentialId!: number | null;
}
export class DestinationCalendarsOutputResponseDto {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@Expose()
@ValidateNested()
@Type(() => DestinationCalendarsOutputDto)
data!: DestinationCalendarsOutputDto;
}
@@ -0,0 +1,50 @@
import { ConnectedCalendar, Calendar } from "@/ee/calendars/outputs/connected-calendars.output";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
@Injectable()
export class DestinationCalendarsService {
constructor(
private readonly calendarsService: CalendarsService,
private readonly destinationCalendarsRepository: DestinationCalendarsRepository
) {}
async updateDestinationCalendars(integration: string, externalId: string, userId: number) {
const userCalendars = await this.calendarsService.getCalendars(userId);
const allCalendars = userCalendars.connectedCalendars
.map((cal: ConnectedCalendar) => cal.calendars ?? [])
.flat();
const credentialId = allCalendars.find(
(cal: Calendar) =>
cal.externalId === externalId && cal.integration === integration && cal.readOnly === false
)?.credentialId;
if (!credentialId) {
throw new NotFoundException(`Could not find calendar ${externalId}`);
}
const primaryEmail =
allCalendars.find((cal: Calendar) => cal.primary && cal.credentialId === credentialId)?.email ?? null;
const {
integration: updatedCalendarIntegration,
externalId: updatedCalendarExternalId,
credentialId: updatedCalendarCredentialId,
userId: updatedCalendarUserId,
} = await this.destinationCalendarsRepository.updateCalendar(
integration,
externalId,
credentialId,
userId,
primaryEmail
);
return {
userId: updatedCalendarUserId,
integration: updatedCalendarIntegration,
externalId: updatedCalendarExternalId,
credentialId: updatedCalendarCredentialId,
};
}
}
@@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common";
import { sendSignupToOrganizationEmail, getTranslation } from "@calcom/platform-libraries-0.0.23";
import { sendSignupToOrganizationEmail, getTranslation } from "@calcom/platform-libraries";
@Injectable()
export class EmailService {
@@ -1,5 +1,6 @@
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
import { BillingModule } from "@/modules/billing/billing.module";
import { DestinationCalendarsModule } from "@/modules/destination-calendars/destination-calendars.module";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { TimezoneModule } from "@/modules/timezones/timezones.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
@@ -16,6 +17,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
TimezoneModule,
UsersModule,
WebhooksModule,
DestinationCalendarsModule,
],
})
export class EndpointsModule implements NestModule {
@@ -0,0 +1,281 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { UserWithProfile } from "@/modules/users/users.repository";
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
import {
OAuthClientWebhooksOutputResponseDto,
OAuthClientWebhookOutputResponseDto,
} from "@/modules/webhooks/outputs/oauth-client-webhook.output";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { PlatformBillingRepositoryFixture } from "test/fixtures/repository/billing.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
import { withNextAuth } from "test/utils/withNextAuth";
import { PlatformOAuthClient, Team, Webhook } from "@calcom/prisma/client";
describe("EventTypes WebhooksController (e2e)", () => {
let app: INestApplication;
const userEmail = "event-types-webhook-controller-e2e@api.com";
const otherUserEmail = "other-event-types-webhook-controller-e2e@api.com";
let user: UserWithProfile;
let otherUser: UserWithProfile;
let oAuthClient: PlatformOAuthClient;
let otherOAuthClient: PlatformOAuthClient;
let org: Team;
let otherOrg: Team;
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
let userRepositoryFixture: UserRepositoryFixture;
let webhookRepositoryFixture: WebhookRepositoryFixture;
let otherOAuthClientWebhook: Webhook;
let membershipRepositoryFixture: MembershipRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let orgRepositoryFixture: OrganizationRepositoryFixture;
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
let webhook: OAuthClientWebhookOutputResponseDto["data"];
beforeAll(async () => {
const moduleRef = await withNextAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
webhookRepositoryFixture = new WebhookRepositoryFixture(moduleRef);
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
orgRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
});
otherUser = await userRepositoryFixture.create({
email: otherUserEmail,
username: otherUserEmail,
});
org = await orgRepositoryFixture.create({
name: "apiOrg",
isOrganization: true,
metadata: {
isOrganization: true,
orgAutoAcceptEmail: "api.com",
isOrganizationVerified: true,
isOrganizationConfigured: true,
},
isPlatform: true,
});
otherOrg = await orgRepositoryFixture.create({
name: "otherOrg",
isOrganization: true,
metadata: {
isOrganization: true,
orgAutoAcceptEmail: "api.com",
isOrganizationVerified: true,
isOrganizationConfigured: true,
},
isPlatform: true,
});
await platformBillingRepositoryFixture.create(org.id);
await platformBillingRepositoryFixture.create(otherOrg.id);
await membershipRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: user.id } },
team: { connect: { id: org.id } },
accepted: true,
});
await membershipRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: otherUser.id } },
team: { connect: { id: otherOrg.id } },
accepted: true,
});
await profileRepositoryFixture.create({
uid: `usr-${user.id}`,
username: userEmail,
organization: {
connect: {
id: org.id,
},
},
movedFromUser: {
connect: {
id: user.id,
},
},
user: {
connect: { id: user.id },
},
});
await profileRepositoryFixture.create({
uid: `usr-${otherUser.id}`,
username: otherUserEmail,
organization: {
connect: {
id: otherOrg.id,
},
},
movedFromUser: {
connect: {
id: otherUser.id,
},
},
user: {
connect: { id: otherUser.id },
},
});
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["redirect-uri"],
permissions: 32,
};
const secret = "secret";
oAuthClient = await oAuthClientRepositoryFixture.create(org.id, data, secret);
otherOAuthClient = await oAuthClientRepositoryFixture.create(otherOrg.id, data, secret);
otherOAuthClientWebhook = await webhookRepositoryFixture.create({
id: "123abc-123abc-123abc-123abc",
active: true,
payloadTemplate: "string",
subscriberUrl: "https://example.com",
eventTriggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
platformOAuthClient: { connect: { id: otherOAuthClient.id } },
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await orgRepositoryFixture.delete(org.id);
await userRepositoryFixture.deleteByEmail(otherUser.email);
await orgRepositoryFixture.delete(otherOrg.id);
await app.close();
});
it("/webhooks (POST)", () => {
return request(app.getHttpServer())
.post(`/v2/oauth-clients/${oAuthClient.id}/webhooks`)
.send({
subscriberUrl: "https://example.com",
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
active: true,
payloadTemplate: "string",
} satisfies CreateWebhookInputDto)
.expect(201)
.then(async (res) => {
expect(res.body).toMatchObject({
status: "success",
data: {
id: expect.any(String),
subscriberUrl: "https://example.com",
active: true,
oAuthClientId: oAuthClient.id,
payloadTemplate: "string",
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
},
} satisfies OAuthClientWebhookOutputResponseDto);
webhook = res.body.data;
});
});
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (PATCH)", () => {
return request(app.getHttpServer())
.patch(`/v2/oauth-clients/${oAuthClient.id}/webhooks/${webhook.id}`)
.send({
active: false,
} satisfies UpdateWebhookInputDto)
.expect(200)
.then((res) => {
expect(res.body.data.active).toBe(false);
});
});
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (GET)", () => {
return request(app.getHttpServer())
.get(`/v2/oauth-clients/${oAuthClient.id}/webhooks/${webhook.id}`)
.expect(200)
.then((res) => {
expect(res.body).toMatchObject({
status: "success",
data: {
id: expect.any(String),
subscriberUrl: "https://example.com",
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
active: false,
payloadTemplate: "string",
oAuthClientId: oAuthClient.id,
},
} satisfies OAuthClientWebhookOutputResponseDto);
});
});
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (GET) should fail to get a webhook that does not exist", () => {
return request(app.getHttpServer()).get(`/v2/oauth-clients/${oAuthClient.id}/webhooks/90284`).expect(404);
});
it("/webhooks (GET)", () => {
return request(app.getHttpServer())
.get(`/v2/oauth-clients/${oAuthClient.id}/webhooks`)
.expect(200)
.then((res) => {
const responseBody = res.body as OAuthClientWebhooksOutputResponseDto;
responseBody.data.forEach((webhook) => {
expect(webhook.oAuthClientId).toBe(oAuthClient.id);
});
});
});
it("/webhooks (GET) should fail to get webhooks of oauth client that doesn't belong to you", () => {
return request(app.getHttpServer()).get(`/v2/oauth-clients/${otherOAuthClient.id}/webhooks`).expect(403);
});
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (DELETE)", () => {
return request(app.getHttpServer())
.delete(`/v2/oauth-clients/${oAuthClient.id}/webhooks/${webhook.id}`)
.expect(200)
.then((res) => {
expect(res.body).toMatchObject({
status: "success",
data: {
id: expect.any(String),
subscriberUrl: "https://example.com",
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
active: false,
payloadTemplate: "string",
oAuthClientId: oAuthClient.id,
},
} satisfies OAuthClientWebhookOutputResponseDto);
});
});
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (DELETE) should fail to delete webhooks of an oauth client that doesn't belong to you", () => {
return request(app.getHttpServer())
.delete(`/v2/oauth-clients/${otherOAuthClient.id}/webhooks/${otherOAuthClientWebhook.id}`)
.expect(403);
});
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
return request(app.getHttpServer())
.delete(`/v2/oauth-clients/${oAuthClient.id}/webhooks/1234453`)
.expect(404);
});
});
@@ -0,0 +1,140 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
import { GetWebhook } from "@/modules/webhooks/decorators/get-webhook-decorator";
import { IsOAuthClientWebhookGuard } from "@/modules/webhooks/guards/is-oauth-client-webhook-guard";
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
import {
OAuthClientWebhookOutputResponseDto,
OAuthClientWebhookOutputDto,
OAuthClientWebhooksOutputResponseDto,
} from "@/modules/webhooks/outputs/oauth-client-webhook.output";
import { DeleteManyWebhooksOutputResponseDto } from "@/modules/webhooks/outputs/webhook.output";
import { PartialWebhookInputPipe, WebhookInputPipe } from "@/modules/webhooks/pipes/WebhookInputPipe";
import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
import { OAuthClientWebhooksService } from "@/modules/webhooks/services/oauth-clients-webhooks.service";
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
import { Controller, Post, Body, UseGuards, Get, Param, Query, Delete, Patch } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { Webhook, MembershipRole } from "@prisma/client";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { SkipTakePagination } from "@calcom/platform-types";
import { OAuthClientGuard } from "../../guards/oauth-client-guard";
@Controller({
path: "/v2/oauth-clients/:clientId/webhooks",
version: API_VERSIONS_VALUES,
})
@UseGuards(NextAuthGuard, OrganizationRolesGuard, OAuthClientGuard)
@ApiTags("OAuthClients Webhooks")
export class OAuthClientWebhooksController {
constructor(
private readonly webhooksService: WebhooksService,
private readonly oAuthClientWebhooksService: OAuthClientWebhooksService
) {}
@Post("/")
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
@ApiOperation({ summary: "Create a webhook for an oAuthClient" })
async createOAuthClientWebhook(
@Body() body: CreateWebhookInputDto,
@Param("clientId") oAuthClientId: string
): Promise<OAuthClientWebhookOutputResponseDto> {
const webhook = await this.oAuthClientWebhooksService.createOAuthClientWebhook(
oAuthClientId,
new WebhookInputPipe().transform(body)
);
return {
status: SUCCESS_STATUS,
data: plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
}),
};
}
@Patch("/:webhookId")
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
@ApiOperation({ summary: "Update a webhook of an oAuthClient" })
@UseGuards(IsOAuthClientWebhookGuard)
async updateOAuthClientWebhook(
@Body() body: UpdateWebhookInputDto,
@Param("webhookId") webhookId: string
): Promise<OAuthClientWebhookOutputResponseDto> {
const webhook = await this.webhooksService.updateWebhook(
webhookId,
new PartialWebhookInputPipe().transform(body)
);
return {
status: SUCCESS_STATUS,
data: plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
}),
};
}
@Get("/:webhookId")
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
@ApiOperation({ summary: "Get a webhook of an oAuthClient" })
@UseGuards(IsOAuthClientWebhookGuard)
async getOAuthClientWebhook(@GetWebhook() webhook: Webhook): Promise<OAuthClientWebhookOutputResponseDto> {
return {
status: SUCCESS_STATUS,
data: plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
}),
};
}
@Get("/")
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
@ApiOperation({ summary: "Get all webhooks of an oAuthClient" })
async getOAuthClientWebhooks(
@Param("clientId") oAuthClientId: string,
@Query() pagination: SkipTakePagination
): Promise<OAuthClientWebhooksOutputResponseDto> {
const webhooks = await this.oAuthClientWebhooksService.getOAuthClientWebhooksPaginated(
oAuthClientId,
pagination.skip ?? 0,
pagination.take ?? 250
);
return {
status: SUCCESS_STATUS,
data: webhooks.map((webhook) =>
plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
})
),
};
}
@Delete("/:webhookId")
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
@ApiOperation({ summary: "Delete a webhook of an oAuthClient" })
@UseGuards(IsOAuthClientWebhookGuard)
async deleteOAuthClientWebhook(
@GetWebhook() webhook: Webhook
): Promise<OAuthClientWebhookOutputResponseDto> {
await this.webhooksService.deleteWebhook(webhook.id);
return {
status: SUCCESS_STATUS,
data: plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
}),
};
}
@Delete("/")
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
@ApiOperation({ summary: "Delete all webhooks of an oAuthClient" })
async deleteAllOAuthClientWebhooks(
@Param("clientId") oAuthClientId: string
): Promise<DeleteManyWebhooksOutputResponseDto> {
const data = await this.oAuthClientWebhooksService.deleteAllOAuthClientWebhooks(oAuthClientId);
return { status: SUCCESS_STATUS, data: `${data.count} webhooks deleted` };
}
}
@@ -37,8 +37,7 @@ import {
ApiOperation as DocsOperation,
ApiCreatedResponse as DocsCreatedResponse,
} from "@nestjs/swagger";
import { MembershipRole } from "@prisma/client";
import { User } from "@prisma/client";
import { User, MembershipRole } from "@prisma/client";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { CreateOAuthClientInput } from "@calcom/platform-types";
@@ -86,6 +85,7 @@ export class OAuthClientsController {
}
const { id, secret } = await this.oauthClientRepository.createOAuthClient(organizationId, body);
return {
status: SUCCESS_STATUS,
data: {
@@ -116,6 +116,7 @@ export class OAuthClientsController {
if (!client) {
throw new NotFoundException(`OAuth client with ID ${clientId} not found`);
}
return { status: SUCCESS_STATUS, data: client };
}
@@ -149,6 +150,7 @@ export class OAuthClientsController {
): Promise<GetOAuthClientResponseDto> {
this.logger.log(`For client ${clientId} updating OAuth Client with data: ${JSON.stringify(body)}`);
const client = await this.oauthClientRepository.updateOAuthClient(clientId, body);
return { status: SUCCESS_STATUS, data: client };
}
@@ -8,7 +8,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, Injectable } from "@nestjs/common";
import { User } from "@prisma/client";
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries-0.0.23";
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
@Injectable()
export class OAuthClientUsersService {
@@ -24,6 +24,7 @@ import {
TeamEventTypeOutput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/enums/booking-window.enum";
import { Team } from "@calcom/prisma/client";
describe("Organizations Event Types Endpoints", () => {
@@ -260,6 +261,21 @@ describe("Organizations Event Types Endpoints", () => {
userId: teammate2.id,
},
],
bookingLimitsCount: {
day: 2,
week: 5,
},
onlyShowFirstAvailableSlot: true,
bookingLimitsDuration: {
day: 60,
week: 100,
},
offsetStart: 30,
bookingWindow: {
type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays,
value: 30,
rolling: true,
},
};
return request(app.getHttpServer())
@@ -276,6 +292,11 @@ describe("Organizations Event Types Endpoints", () => {
expect(data.schedulingType).toEqual("COLLECTIVE");
evaluateHost(body.hosts[0], data.hosts[0]);
evaluateHost(body.hosts[1], data.hosts[1]);
expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount);
expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
expect(data.offsetStart).toEqual(body.offsetStart);
expect(data.bookingWindow).toEqual(body.bookingWindow);
collectiveEventType = responseBody.data;
});
@@ -23,7 +23,15 @@ export class InputOrganizationsEventTypesService {
teamId: number,
inputEventType: CreateTeamEventTypeInput_2024_06_14
) {
const { hosts, assignAllTeamMembers, ...rest } = inputEventType;
const {
hosts,
assignAllTeamMembers,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookingFields,
...rest
} = inputEventType;
const eventType = this.inputEventTypesService.transformInputCreateEventType(rest);
@@ -35,6 +43,10 @@ export class InputOrganizationsEventTypesService {
? await this.getAllTeamMembers(teamId, inputEventType.schedulingType)
: this.transformInputHosts(hosts, inputEventType.schedulingType),
assignAllTeamMembers,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookingFields,
metadata,
};
@@ -6,10 +6,11 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/repositori
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.23";
import { createEventType, updateEventType } from "@calcom/platform-libraries";
import {
CreateTeamEventTypeInput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
@@ -25,7 +26,8 @@ export class OrganizationsEventTypesService {
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly outputService: OutputOrganizationsEventTypesService,
private readonly membershipsRepository: MembershipsRepository,
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository,
private readonly usersService: UsersService
) {}
async createTeamEventType(
@@ -36,11 +38,19 @@ export class OrganizationsEventTypesService {
) {
await this.validateHosts(teamId, body.hosts);
const eventTypeUser = await this.getUserToCreateTeamEvent(user, orgId);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { hosts, assignAllTeamMembers, ...rest } =
await this.inputService.transformInputCreateTeamEventType(teamId, body);
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
hosts,
assignAllTeamMembers,
locations,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookingFields,
...rest
} = await this.inputService.transformInputCreateTeamEventType(teamId, body);
const { eventType: eventTypeCreated } = await createEventType({
input: { teamId: teamId, ...rest },
input: { teamId: teamId, locations, ...rest },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -52,7 +62,15 @@ export class OrganizationsEventTypesService {
return await this.updateTeamEventType(
eventTypeCreated.id,
teamId,
{ hosts: body.hosts, assignAllTeamMembers },
{
hosts: body.hosts,
assignAllTeamMembers,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookingFields,
...rest,
},
user
);
}
@@ -77,7 +95,8 @@ export class OrganizationsEventTypesService {
async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) {
const isOrgAdmin = await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId);
const profileId = user.movedToProfileId || null;
const profileId =
this.usersService.getUserProfileByOrgId(user, organizationId)?.id || user.movedToProfileId;
return {
id: user.id,
role: user.role,
@@ -3,9 +3,9 @@ import { OrganizationsEventTypesRepository } from "@/modules/organizations/repos
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import type { EventType, User, Schedule, Host } from "@prisma/client";
import { SchedulingType } from "@prisma/client";
import { HostPriority, TeamEventTypeResponseHost } from "@calcom/platform-types";
import { SchedulingType } from "@calcom/prisma/enums";
type EventTypeRelations = { users: User[]; schedule: Schedule | null; hosts: Host[] };
type DatabaseEventType = EventType & EventTypeRelations;
@@ -43,6 +43,15 @@ type Input = Pick<
| "userId"
| "parentId"
| "assignAllTeamMembers"
| "bookingLimits"
| "durationLimits"
| "onlyShowFirstAvailableSlot"
| "offsetStart"
| "periodType"
| "periodDays"
| "periodCountCalendarDays"
| "periodStartDate"
| "periodEndDate"
>;
@Injectable()
@@ -5,7 +5,7 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/repositori
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries-0.0.23";
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries";
@Injectable()
export class OrganizationsTeamsService {
@@ -7,7 +7,7 @@ import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
import { Injectable, ConflictException } from "@nestjs/common";
import { plainToInstance } from "class-transformer";
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries-0.0.23";
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries";
import { Team } from "@calcom/prisma/client";
@Injectable()
@@ -5,8 +5,8 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { getAvailableSlots } from "@calcom/platform-libraries-0.0.23";
import type { AvailableSlotsType } from "@calcom/platform-libraries-0.0.23";
import { getAvailableSlots } from "@calcom/platform-libraries";
import type { AvailableSlotsType } from "@calcom/platform-libraries";
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
import { ApiResponse, GetAvailableSlotsInput } from "@calcom/platform-types";
@@ -3,7 +3,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { DateTime } from "luxon";
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries-0.0.23";
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries";
import { ReserveSlotInput } from "@calcom/platform-types";
@Injectable()
@@ -4,7 +4,7 @@ import { Controller, Get } from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { CityTimezones } from "@calcom/platform-libraries-0.0.23";
import type { CityTimezones } from "@calcom/platform-libraries";
import { ApiResponse } from "@calcom/platform-types";
@Controller({
@@ -1,8 +1,8 @@
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable } from "@nestjs/common";
import { cityTimezonesHandler } from "@calcom/platform-libraries-0.0.23";
import type { CityTimezones } from "@calcom/platform-libraries-0.0.23";
import { cityTimezonesHandler } from "@calcom/platform-libraries";
import type { CityTimezones } from "@calcom/platform-libraries";
@Injectable()
export class TimezonesService {
@@ -1,4 +1,4 @@
import { UsersRepository } from "@/modules/users/users.repository";
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { User } from "@calcom/prisma/client";
@@ -21,4 +21,16 @@ export class UsersService {
return users;
}
getUserMainProfile(user: UserWithProfile) {
return (
user?.movedToProfile ||
user.profiles?.find((p) => p.organizationId === user.organizationId) ||
user.profiles?.[0]
);
}
getUserProfileByOrgId(user: UserWithProfile, organizationId: number) {
return user.profiles?.find((p) => p.organizationId === organizationId);
}
}
@@ -3,10 +3,11 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { Injectable } from "@nestjs/common";
import type { Profile, User } from "@prisma/client";
import type { Profile, User, Team } from "@prisma/client";
export type UserWithProfile = User & {
movedToProfile?: Profile | null;
movedToProfile?: (Profile & { organization: Pick<Team, "isPlatform" | "id" | "slug" | "name"> }) | null;
profiles?: (Profile & { organization: Pick<Team, "isPlatform" | "id" | "slug" | "name"> })[];
};
@Injectable()
@@ -67,12 +68,18 @@ export class UsersRepository {
}
async findByIdWithProfile(userId: number): Promise<UserWithProfile | null> {
console.log("findByIdWithProfile");
return this.dbRead.prisma.user.findUnique({
where: {
id: userId,
},
include: {
movedToProfile: true,
movedToProfile: {
include: { organization: { select: { isPlatform: true, name: true, slug: true, id: true } } },
},
profiles: {
include: { organization: { select: { isPlatform: true, name: true, slug: true, id: true } } },
},
},
});
}
@@ -126,7 +133,12 @@ export class UsersRepository {
email,
},
include: {
movedToProfile: true,
movedToProfile: {
include: { organization: { select: { isPlatform: true, name: true, slug: true, id: true } } },
},
profiles: {
include: { organization: { select: { isPlatform: true, name: true, slug: true, id: true } } },
},
},
});
}
@@ -0,0 +1,69 @@
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
import {
CanActivate,
ExecutionContext,
Injectable,
NotFoundException,
ForbiddenException,
BadRequestException,
} from "@nestjs/common";
import { Request } from "express";
import { PlatformOAuthClient, Webhook } from "@calcom/prisma/client";
@Injectable()
export class IsOAuthClientWebhookGuard implements CanActivate {
constructor(
private readonly webhooksService: WebhooksService,
private readonly oAuthClientRepository: OAuthClientRepository
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context
.switchToHttp()
.getRequest<Request & { webhook: Webhook; oAuthClient: PlatformOAuthClient }>();
const user = request.user as GetUserReturnType;
const webhookId = request.params.webhookId;
const oAuthClientId = request.params.clientId;
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
if (!user) {
throw new ForbiddenException("User not authenticated");
}
if (!webhookId) {
throw new BadRequestException("webhookId parameter not specified in the request");
}
if (!webhookId) {
throw new BadRequestException("oAuthClientId parameter not specified in the request");
}
if (!user || !webhookId || !oAuthClientId) {
return false;
}
const oAuthClient = await this.oAuthClientRepository.getOAuthClient(oAuthClientId);
if (!oAuthClient) {
throw new NotFoundException(`OAuthClient (${oAuthClientId}) not found`);
}
const webhook = await this.webhooksService.getWebhookById(webhookId);
if (oAuthClient?.organizationId !== organizationId) {
return user.isSystemAdmin;
}
if (webhook.platformOAuthClientId !== oAuthClientId) {
throw new ForbiddenException("Webhook does not belong to this oAuthClient");
}
request.webhook = webhook;
request.oAuthClient = oAuthClient;
return true;
}
}
@@ -0,0 +1,26 @@
import { Expose, Type } from "class-transformer";
import { IsInt, ValidateNested } from "class-validator";
import { ApiResponseWithoutData } from "@calcom/platform-types";
import { WebhookOutputDto } from "./webhook.output";
export class OAuthClientWebhookOutputDto extends WebhookOutputDto {
@IsInt()
@Expose()
readonly oAuthClientId!: string;
}
export class OAuthClientWebhookOutputResponseDto extends ApiResponseWithoutData {
@Expose()
@ValidateNested()
@Type(() => WebhookOutputDto)
data!: OAuthClientWebhookOutputDto;
}
export class OAuthClientWebhooksOutputResponseDto extends ApiResponseWithoutData {
@Expose()
@ValidateNested()
@Type(() => WebhookOutputDto)
data!: OAuthClientWebhookOutputDto[];
}
@@ -1,7 +1,7 @@
import { ApiProperty } from "@nestjs/swagger";
import { WebhookTriggerEvents } from "@prisma/client";
import { Expose, Type } from "class-transformer";
import { IsBoolean, IsEnum, IsInt, IsString, ValidateNested } from "class-validator";
import { IsBoolean, IsEnum, IsInt, IsString, ValidateNested, IsArray } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
@@ -25,7 +25,8 @@ export class WebhookOutputDto {
})
readonly payloadTemplate!: string;
@IsEnum(WebhookTriggerEvents)
@IsArray()
@IsEnum(WebhookTriggerEvents, { each: true })
@Expose()
readonly triggers!: WebhookTriggerEvents[];
@@ -36,6 +37,10 @@ export class WebhookOutputDto {
@IsBoolean()
@Expose()
readonly active!: boolean;
@IsString()
@Expose()
readonly secret?: string;
}
export class DeleteManyWebhooksOutputResponseDto {
@@ -4,13 +4,8 @@ import { Webhook } from "@prisma/client";
@Injectable()
export class WebhookOutputPipe implements PipeTransform {
transform(value: Webhook) {
if (value?.eventTriggers) {
const { eventTriggers, ...rest } = value;
const triggers = eventTriggers;
const parsedData = { ...rest, triggers };
return parsedData;
}
return value;
const { eventTriggers, platformOAuthClientId, ...rest } = value;
return { ...rest, triggers: eventTriggers, oAuthClientId: platformOAuthClientId };
}
}
@@ -0,0 +1,32 @@
import { PipedInputWebhookType } from "@/modules/webhooks/pipes/WebhookInputPipe";
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
import { ConflictException, Injectable } from "@nestjs/common";
@Injectable()
export class OAuthClientWebhooksService {
constructor(private readonly webhooksRepository: WebhooksRepository) {}
async createOAuthClientWebhook(platformOAuthClientId: string, body: PipedInputWebhookType) {
const existingWebhook = await this.webhooksRepository.getOAuthClientWebhookByUrl(
platformOAuthClientId,
body.subscriberUrl
);
if (existingWebhook) {
throw new ConflictException("Webhook with this subscriber url already exists for this oAuth client");
}
return this.webhooksRepository.createOAuthClientWebhook(platformOAuthClientId, {
...body,
payloadTemplate: body.payloadTemplate ?? null,
secret: body.secret ?? null,
});
}
async getOAuthClientWebhooksPaginated(platformOAuthClientId: string, skip: number, take: number) {
return this.webhooksRepository.getOAuthClientWebhooksPaginated(platformOAuthClientId, skip, take);
}
async deleteAllOAuthClientWebhooks(platformOAuthClientId: string): Promise<{ count: number }> {
return this.webhooksRepository.deleteAllOAuthClientWebhooks(platformOAuthClientId);
}
}
@@ -1,19 +1,44 @@
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { EventTypeWebhooksController } from "@/modules/event-types/controllers/event-types-webhooks.controller";
import { OAuthClientWebhooksController } from "@/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { Module } from "@nestjs/common";
import { MembershipsModule } from "../memberships/memberships.module";
import { OrganizationsModule } from "../organizations/organizations.module";
import { PrismaModule } from "../prisma/prisma.module";
import { UsersModule } from "../users/users.module";
import { WebhooksController } from "./controllers/webhooks.controller";
import { EventTypeWebhooksService } from "./services/event-type-webhooks.service";
import { OAuthClientWebhooksService } from "./services/oauth-clients-webhooks.service";
import { UserWebhooksService } from "./services/user-webhooks.service";
import { WebhooksService } from "./services/webhooks.service";
import { WebhooksRepository } from "./webhooks.repository";
@Module({
imports: [PrismaModule, UsersModule, EventTypesModule_2024_06_14],
controllers: [WebhooksController, EventTypeWebhooksController],
providers: [WebhooksService, WebhooksRepository, UserWebhooksService, EventTypeWebhooksService],
exports: [WebhooksService, WebhooksRepository, UserWebhooksService, EventTypeWebhooksService],
imports: [
PrismaModule,
UsersModule,
EventTypesModule_2024_06_14,
OAuthClientModule,
OrganizationsModule,
MembershipsModule,
OAuthClientModule,
],
controllers: [WebhooksController, EventTypeWebhooksController, OAuthClientWebhooksController],
providers: [
WebhooksService,
WebhooksRepository,
UserWebhooksService,
EventTypeWebhooksService,
OAuthClientWebhooksService,
],
exports: [
WebhooksService,
WebhooksRepository,
UserWebhooksService,
EventTypeWebhooksService,
OAuthClientWebhooksService,
],
})
export class WebhooksModule {}
@@ -29,6 +29,13 @@ export class WebhooksRepository {
});
}
async createOAuthClientWebhook(platformOAuthClientId: string, data: WebhookInputData) {
const id = uuidv4();
return this.dbWrite.prisma.webhook.create({
data: { ...data, id, platformOAuthClientId },
});
}
async updateWebhook(webhookId: string, data: Partial<WebhookInputData>) {
return this.dbWrite.prisma.webhook.update({
where: { id: webhookId },
@@ -58,12 +65,26 @@ export class WebhooksRepository {
});
}
async getOAuthClientWebhooksPaginated(platformOAuthClientId: string, skip: number, take: number) {
return this.dbRead.prisma.webhook.findMany({
where: { platformOAuthClientId },
skip,
take,
});
}
async getUserWebhookByUrl(userId: number, subscriberUrl: string) {
return this.dbRead.prisma.webhook.findFirst({
where: { userId, subscriberUrl },
});
}
async getOAuthClientWebhookByUrl(platformOAuthClientId: string, subscriberUrl: string) {
return this.dbRead.prisma.webhook.findFirst({
where: { platformOAuthClientId, subscriberUrl },
});
}
async getEventTypeWebhookByUrl(eventTypeId: number, subscriberUrl: string) {
return this.dbRead.prisma.webhook.findFirst({
where: { eventTypeId, subscriberUrl },
@@ -81,4 +102,10 @@ export class WebhooksRepository {
where: { eventTypeId },
});
}
async deleteAllOAuthClientWebhooks(oAuthClientId: string) {
return this.dbWrite.prisma.webhook.deleteMany({
where: { platformOAuthClientId: oAuthClientId },
});
}
}
+480 -37
View File
@@ -3594,6 +3594,238 @@
"Users' EventTypes Webhooks"
]
}
},
"/v2/oauth-clients/{clientId}/webhooks": {
"post": {
"operationId": "OAuthClientWebhooksController_createOAuthClientWebhook",
"summary": "Create a webhook for an oAuthClient",
"parameters": [
{
"name": "clientId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateWebhookInputDto"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto"
}
}
}
}
},
"tags": [
"OAuthClients Webhooks"
]
},
"get": {
"operationId": "OAuthClientWebhooksController_getOAuthClientWebhooks",
"summary": "Get all webhooks of an oAuthClient",
"parameters": [
{
"name": "clientId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "The number of items to return",
"example": 10,
"schema": {
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "The number of items to skip",
"example": 0,
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthClientWebhooksOutputResponseDto"
}
}
}
}
},
"tags": [
"OAuthClients Webhooks"
]
},
"delete": {
"operationId": "OAuthClientWebhooksController_deleteAllOAuthClientWebhooks",
"summary": "Delete all webhooks of an oAuthClient",
"parameters": [
{
"name": "clientId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteManyWebhooksOutputResponseDto"
}
}
}
}
},
"tags": [
"OAuthClients Webhooks"
]
}
},
"/v2/oauth-clients/{clientId}/webhooks/{webhookId}": {
"patch": {
"operationId": "OAuthClientWebhooksController_updateOAuthClientWebhook",
"summary": "Update a webhook of an oAuthClient",
"parameters": [
{
"name": "webhookId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateWebhookInputDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto"
}
}
}
}
},
"tags": [
"OAuthClients Webhooks"
]
},
"get": {
"operationId": "OAuthClientWebhooksController_getOAuthClientWebhook",
"summary": "Get a webhook of an oAuthClient",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto"
}
}
}
}
},
"tags": [
"OAuthClients Webhooks"
]
},
"delete": {
"operationId": "OAuthClientWebhooksController_deleteOAuthClientWebhook",
"summary": "Delete a webhook of an oAuthClient",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto"
}
}
}
}
},
"tags": [
"OAuthClients Webhooks"
]
}
},
"/v2/destination-calendars": {
"put": {
"operationId": "DestinationCalendarsController_updateDestinationCalendars",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DestinationCalendarsInputBodyDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DestinationCalendarsOutputResponseDto"
}
}
}
}
},
"tags": [
"Destination-Calendars",
"Select a third party destination calendar where events will be created"
]
}
}
},
"info": {
@@ -6399,33 +6631,6 @@
"type": "string",
"example": "Discover the culinary wonders of the Argentina by making the best flan ever!"
},
"locations": {
"type": "array",
"items": {
"type": "string"
}
},
"bookingFields": {
"type": "array",
"items": {
"type": "string"
}
},
"disableGuests": {
"type": "boolean"
},
"slotInterval": {
"type": "number"
},
"minimumBookingNotice": {
"type": "number"
},
"beforeEventBuffer": {
"type": "number"
},
"afterEventBuffer": {
"type": "number"
},
"schedulingType": {
"type": "object"
},
@@ -6443,13 +6648,6 @@
"lengthInMinutes",
"title",
"description",
"locations",
"bookingFields",
"disableGuests",
"slotInterval",
"minimumBookingNotice",
"beforeEventBuffer",
"afterEventBuffer",
"schedulingType",
"hosts",
"assignAllTeamMembers"
@@ -6547,6 +6745,64 @@
"userId"
]
},
"BookingLimitsCount_2024_06_14": {
"type": "object",
"properties": {
"day": {
"type": "number",
"minimum": 1,
"description": "The number of bookings per day",
"example": 1
},
"week": {
"type": "number",
"minimum": 1,
"description": "The number of bookings per week",
"example": 2
},
"month": {
"type": "number",
"minimum": 1,
"description": "The number of bookings per month",
"example": 3
},
"year": {
"type": "number",
"minimum": 1,
"description": "The number of bookings per year",
"example": 4
}
}
},
"BookingLimitsDuration_2024_06_14": {
"type": "object",
"properties": {
"day": {
"type": "number",
"minimum": 15,
"description": "The duration of bookings per day (must be a multiple of 15)",
"example": 60
},
"week": {
"type": "number",
"minimum": 15,
"description": "The duration of bookings per week (must be a multiple of 15)",
"example": 120
},
"month": {
"type": "number",
"minimum": 15,
"description": "The duration of bookings per month (must be a multiple of 15)",
"example": 180
},
"year": {
"type": "number",
"minimum": 15,
"description": "The duration of bookings per year (must be a multiple of 15)",
"example": 240
}
}
},
"TeamEventTypeOutput_2024_06_14": {
"type": "object",
"properties": {
@@ -6666,6 +6922,22 @@
},
"assignAllTeamMembers": {
"type": "boolean"
},
"bookingLimitsCount": {
"$ref": "#/components/schemas/BookingLimitsCount_2024_06_14"
},
"onlyShowFirstAvailableSlot": {
"type": "boolean"
},
"bookingLimitsDuration": {
"$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14"
},
"bookingWindow": {
"type": "object"
},
"offsetStart": {
"type": "number",
"minimum": 1
}
},
"required": [
@@ -7525,6 +7797,21 @@
"status"
]
},
"MeOrgOutput": {
"type": "object",
"properties": {
"isPlatform": {
"type": "boolean"
},
"id": {
"type": "number"
}
},
"required": [
"isPlatform",
"id"
]
},
"MeOutput": {
"type": "object",
"properties": {
@@ -7553,6 +7840,9 @@
"organizationId": {
"type": "number",
"nullable": true
},
"organization": {
"$ref": "#/components/schemas/MeOrgOutput"
}
},
"required": [
@@ -8544,9 +8834,6 @@
"noShowHost": {
"type": "boolean"
},
"messageKey": {
"type": "string"
},
"attendees": {
"type": "array",
"items": {
@@ -8660,6 +8947,9 @@
},
"active": {
"type": "boolean"
},
"secret": {
"type": "string"
}
},
"required": [
@@ -8787,6 +9077,9 @@
},
"active": {
"type": "boolean"
},
"secret": {
"type": "string"
}
},
"required": [
@@ -8860,6 +9153,156 @@
"status",
"data"
]
},
"OAuthClientWebhookOutputDto": {
"type": "object",
"properties": {
"payloadTemplate": {
"type": "string",
"description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
"example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}"
},
"oAuthClientId": {
"type": "string"
},
"id": {
"type": "number"
},
"triggers": {
"type": "array",
"items": {
"type": "object"
}
},
"subscriberUrl": {
"type": "string"
},
"active": {
"type": "boolean"
},
"secret": {
"type": "string"
}
},
"required": [
"payloadTemplate",
"oAuthClientId",
"id",
"triggers",
"subscriberUrl",
"active"
]
},
"OAuthClientWebhookOutputResponseDto": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/OAuthClientWebhookOutputDto"
}
},
"required": [
"status",
"data"
]
},
"OAuthClientWebhooksOutputResponseDto": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/OAuthClientWebhookOutputDto"
}
}
},
"required": [
"status",
"data"
]
},
"DestinationCalendarsInputBodyDto": {
"type": "object",
"properties": {
"integration": {
"type": "string",
"example": "apple_calendar",
"description": "The calendar service you want to integrate, as returned by the /calendars endpoint",
"enum": [
"apple_calendar",
"google_calendar",
"office365_calendar"
]
},
"externalId": {
"type": "string",
"example": "https://caldav.icloud.com/26962146906/calendars/1644422A-1945-4438-BBC0-4F0Q23A57R7S/",
"description": "Unique identifier used to represent the specfic calendar, as returned by the /calendars endpoint"
}
},
"required": [
"integration",
"externalId"
]
},
"DestinationCalendarsOutputDto": {
"type": "object",
"properties": {
"userId": {
"type": "number"
},
"integration": {
"type": "string"
},
"externalId": {
"type": "string"
},
"credentialId": {
"type": "number",
"nullable": true
}
},
"required": [
"userId",
"integration",
"externalId",
"credentialId"
]
},
"DestinationCalendarsOutputResponseDto": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/DestinationCalendarsOutputDto"
}
},
"required": [
"status",
"data"
]
}
}
}
@@ -34,14 +34,15 @@ export class EventTypesRepositoryFixture {
});
}
async create(
data: Pick<CreateEventTypeInput_2024_04_15, "title" | "slug" | "length" | "hidden">,
userId: number
) {
async create(data: Prisma.EventTypeCreateInput, userId: number) {
return this.prismaWriteClient.eventType.create({
data: {
...data,
userId,
owner: {
connect: {
id: userId,
},
},
},
});
}
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;
+2
View File
@@ -5,6 +5,7 @@ import { headers, cookies } from "next/headers";
import React from "react";
import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { IconSprites } from "@calcom/ui";
import { prepareRootMetadata } from "@lib/metadata";
@@ -83,6 +84,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
--font-cal: ${calFont.style.fontFamily.replace(/\'/g, "")};
}
`}</style>
<IconSprites />
</head>
<body
className="dark:bg-darkgray-50 bg-subtle antialiased"
+6 -69
View File
@@ -1,15 +1,14 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import type { ReactNode } from "react";
import { useEffect, useRef, useState } from "react";
import { z } from "zod";
import type { CredentialOwner } from "@calcom/app-store/types";
import classNames from "@calcom/lib/classNames";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { Avatar, Badge, Icon, ListItemText } from "@calcom/ui";
import { AppListCard as AppListCardComponent } from "@calcom/ui";
type ShouldHighlight =
| {
@@ -21,7 +20,7 @@ type ShouldHighlight =
slug?: never;
};
type AppListCardProps = {
export type AppListCardProps = {
logo?: string;
title: string;
description: string;
@@ -37,21 +36,7 @@ type AppListCardProps = {
const schema = z.object({ hl: z.string().optional() });
export default function AppListCard(props: AppListCardProps) {
const { t } = useLocale();
const {
logo,
title,
description,
actions,
isDefault,
slug,
shouldHighlight,
isTemplate,
invalidCredential,
children,
credentialOwner,
className,
} = props;
const { slug, shouldHighlight } = props;
const {
data: { hl },
} = useTypedQuery(schema);
@@ -83,53 +68,5 @@ export default function AppListCard(props: AppListCardProps) {
};
}, [highlight, pathname, router, searchParams, shouldHighlight]);
return (
<div className={classNames(highlight && "dark:bg-muted bg-yellow-100", className)}>
<div className="flex items-center gap-x-3 px-4 py-4 sm:px-6">
{logo ? (
<img
className={classNames(logo.includes("-dark") && "dark:invert", "h-10 w-10")}
src={logo}
alt={`${title} logo`}
/>
) : null}
<div className="flex grow flex-col gap-y-1 truncate">
<div className="flex items-center gap-x-2">
<h3 className="text-emphasis truncate text-sm font-semibold">{title}</h3>
<div className="flex items-center gap-x-2">
{isDefault && <Badge variant="green">{t("default")}</Badge>}
{isTemplate && <Badge variant="red">Template</Badge>}
</div>
</div>
<ListItemText component="p">{description}</ListItemText>
{invalidCredential && (
<div className="flex gap-x-2 pt-2">
<Icon name="circle-alert" className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
{t("invalid_credential")}
</ListItemText>
</div>
)}
</div>
{credentialOwner && (
<div>
<Badge variant="gray">
<div className="flex items-center">
<Avatar
className="mr-2"
alt={credentialOwner.name || "Nameless"}
size="xs"
imageSrc={getPlaceholderAvatar(credentialOwner.avatar, credentialOwner?.name as string)}
/>
{credentialOwner.name}
</div>
</Badge>
</div>
)}
{actions}
</div>
{children && <div className="w-full">{children}</div>}
</div>
);
return <AppListCardComponent {...props} />;
}
+1 -1
View File
@@ -15,7 +15,7 @@ export default function EnterprisePage() {
description: t("branded_subdomain_description"),
},
{
icon: <Icon name="bar-chart" className="h-5 w-5 text-blue-500" />,
icon: <Icon name="chart-bar" className="h-5 w-5 text-blue-500" />,
title: t("org_insights"),
description: t("org_insights_description"),
},

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