Compare commits

..
131 Commits
Author SHA1 Message Date
GitHub Actions 01ca2ca3de chore: release v5.1.13
Post release / release (push) Canceled after 0s
2025-04-01 15:19:27 +00:00
f94e80558a feat: refactor handleCancelBooking to not have next request (#20241)
* refactor handleCancelBooking to not have next request

* fix platform v2 function calls

* dont fall back to "" if no seat refrence

* add missing type from merge

* chore: bump platform libraries

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
2025-04-01 14:52:24 +00:00
Alex van Andel a5c15e6517 chore: Allow tracing orphaned event types (added id) (#20479) 2025-04-01 13:56:32 +00:00
Udit Takkar aaa39cccf8 chore: update status code (#20443)
* chore: update status code

* chore: update status code

* fix: error unhandled
2025-04-01 10:08:14 -03:00
amrit d24a93962f fix: trigger lingo.dev by removing duplicate value (#20475) 2025-04-01 15:38:24 +05:30
Eunjae Lee 1d9d52ecf8 fix: add missing DataTableProvider (#20473)
* fix: add missing DataTableProvider

* add e2e test
2025-04-01 10:57:36 +01:00
sean-brydon f1a832921a fix form builder dark mode (#20474) 2025-04-01 09:30:07 +00:00
amrit af1633db28 fix: trigger Lingo.dev for a translation not picked up (#20472) 2025-04-01 13:30:07 +05:30
Benny Joo 7353b6a604 fix: trigger Lingo.dev for a translation not picked up (#20468) 2025-04-01 02:39:58 -04:00
Anik Dhabal Babu dab0b4a4d8 fix: user fields response not pupulated (#20458) 2025-04-01 06:30:00 +00:00
GitHub Actions 3711e01a77 chore: release v5.1.12
Post release / release (push) Canceled after 0s
2025-03-31 22:17:14 +00:00
Anik Dhabal Babu 63c9cb6b92 test: embed e2e tests for mobile viewport and hideEventTypeDetails=true (#20445)
* test: embed e2e test

* update

* tweak

* Update fixtures.ts

* update

* something new

* Update playwright.config.ts
2025-03-31 23:05:53 +01:00
Eunjae Lee f039e116d7 fix: eventTypeId filter on Download at /insights (#20463)
* fix: eventTypeId filter on Download at /insights

* show toast for error
2025-03-31 22:00:15 +00:00
Joe Au-Yeung 93621baea5 Add users to event types when provisioned (#20465) 2025-03-31 21:49:08 +00:00
Tushar BhattandEunjae Lee eaa17fa48d fix: booking filtering queryParams (#20447)
* fix: bookng-filtering-queryParams

* adding comments

* feedback changes

* fix: typo

* minor-change

* revert Pagination.tsx

* minor-change

* minor-changes

* Update packages/features/data-table/DataTableProvider.tsx

* fix: type-check

* fix: test

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-03-31 17:19:28 +01:00
Tushar Bhatt 9ce94fbbfc fix:theme-flakiness (#20460) 2025-03-31 21:18:06 +05:30
Lauris Skraucis 69548e6ed0 docs: v2 authorization headers (#20453)
* refactor: client secret header for oauth users and webhooks

* fix: platform introduction link to api reference

* regenerate docs for commits above

* refactor: specify request headers

* refactor: specify request headers
2025-03-31 14:28:05 +03:00
sean-brydon 5cd2ba1362 perf: move stripe to billing service out of app store (#20376)
* add more methods to billingService

* update profile handler

* stripe customer handler

* verify email

* move imports + move generic methods to billing service

* move to strip ebilling service class

* push changes to mocks

* fix enum

* fix typo

* correctly update customer metadata

* fix userId type to number

* fix types on billing services

* fix type errors due to return methods differing

* fix return types
2025-03-31 09:20:27 +00:00
Eunjae Lee f15d347804 feat: implement filter segment (#20066)
* feat: add filter segment

* add tests

* add ui

* provide tableIdentifier from context

* fix components

* update save button

* try to fix router

* add proper implementation

* update icon

* remove segmentId if invalid

* fix tests

* fix type errors

* fix type errors

* fix unit test

* clean up

* minor fixes

* clean up types

* avoid barrel imports

* fix imports

* minor improvements

* fix type error

* fix radio import

* fix little issues

* menu permission

* update icon snapshot

* fix: remember last used filter segment (#20261)

* fix: remember last used filter segment

* extract segments logic as a separate file

* remove unnecessary query

* apply feedback

* deep comparison
2025-03-31 08:53:48 +00:00
Morgan c95118715a chore: don't log the whole delegated credential (#20452) 2025-03-31 08:25:35 +00:00
Benny Joo 21e7ac3cbe fix: middleware config matcher (#20450)
* fix: middleware config matcher - ending slash

* improve comment
2025-03-30 23:02:20 -03:00
Amit SharmaandKeith Williams 81de95bd28 feat: allow plain ask a question for paid users only (#20390)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-30 04:17:12 +00:00
Omar López d80ed07093 fix: calendar cache date mismtach (#20446)
* fix: calendar cache date mismtach

* Update tests
2025-03-29 17:00:26 -07:00
Anik Dhabal Babu b2ac86c759 fix: send email to is not being displayed in routing forms (#20444)
* feat: ability to set locked timezone

* fix: send email to is not being displayed in routing forms

* update

* Update EventAdvancedTab.tsx
2025-03-29 14:49:05 +00:00
Vachmara 295cf40c6e fix(intercom app): team / org url validation (#20431) 2025-03-29 10:56:07 +05:30
Omar López 92827b2c9d fix: atom builds (#20437) 2025-03-28 23:18:56 +00:00
Omar López e1076690c1 chore: bump platform (#20436) 2025-03-28 21:10:46 +00:00
Calcom BotandLingo.dev 70dd9b9c2c feat: update translations via @lingodotdev (#20432)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-28 20:20:38 +00:00
GitHub Actions 71bfd17f81 chore: release v5.1.11
Post release / release (push) Canceled after 0s
2025-03-28 19:29:33 +00:00
Eunjae Lee 5b08b9f11f fix: add isAdminOrOwner and useIsOrgAdminOrOwner (#20429)
* fix: add isAdminOrOwner and useIsOrgAdminOrOwner

* make useIsOrgAdminOrOwner to accept session

* revert change

* remove useIsOrgAdminOrOwner and rename isAdminOrOwner to checkAdminOrOwner

* fix type

* fix type

* fix import
2025-03-28 17:01:06 +00:00
Keith Williams 3456477012 chore: Upgrade lingo.dev (#20430) 2025-03-28 13:39:58 -03:00
Amit Sharma c654795a3e fix: Follow Up for dub referrral embed (#20427)
* fix: Follow Up for dub referrral embed

* fix meta title and description
2025-03-28 21:57:01 +05:30
Eunjae Lee a7b1981cf1 fix: wrong dates on OOO due to summer time (#20408)
* fix: wrong dates on OOO due to summer time

* update styles

* fix test

* fix edit button
2025-03-28 21:00:56 +05:30
Joe Au-YeungandHariom Balhara 48016e0f3e chore: Move CRM owner calls handleResponse (#19316)
* Handle normal ownership routing

* Skip contact owner check if true

* Fix typos

* Call getting contact owner in promise.all in handleResponse

* Pass crm contact owner data to params

* Add crm contact params

* Only get CRM owner calls if params don't exist

* Handle if properties don't exist

* Fix typo

* Pass CRM specific variables to getServerSideProps

* Remove logs

* Type fixes

* Use `wrapGetServerSIdePropsWIthSentry` in router getServerSideProps

* Add sentry monitoring

* Type fixes

* Type fix

* Type fixes

* Update imports for sentryWrapper

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-03-28 10:42:28 -04:00
30367cd5f3 [Snyk] Security upgrade next from 13.5.7 to 13.5.9 (#20412)
* fix: apps/api/v1/package.json to reduce vulnerabilities

The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-NEXT-9508709

* updated yarn.lock

---------

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-28 14:36:45 +00:00
Joe Au-YeungandUdit Takkar 47d822a092 fix: Dsync group table formatting (#20422)
* Fix column to take up entire table

* Fix formatting of group names

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-03-28 13:24:26 +00:00
Joe Au-Yeung b597dad450 feat: Salesforce - Fallback to Lead if Contact Isn't Found For Routing (#20423)
* Add fallback to lead to schema

* FE option to fallback to lead owner

* Logic to query for contacts and leads for RR skip
2025-03-28 16:54:03 +05:30
Lauris Skraucis e67e073f04 refactor: v2 team event type assignAllTeamMembers (#20400)
* fix: platform seed data

* fix: assignAllTeamMembers: true excludes team owners in platform

* refactor: platform plan guard explicit error message

* refactor: require either hosts or assignAllTeamMembers=true

* refactor: require either hosts or assignAllTeamMembers=true

* fix: before creating or updating team event type check if doesnt exist with that slug

* chore: regenerate docs

* test: can update round robin that had assign all team members true

* fix: platform guard spec test

* fix: seed platform membership role

* fix: test ts error
2025-03-28 05:56:26 -03:00
Hariom Balhara 17f493a488 fix: 500 error on missing response id. Should be 400 (#20420)
* fix: 500 error on missing response id. Should be 400

* fix failing test
2025-03-28 07:53:32 +00:00
Hariom Balhara 96fcf7beec fix: Flaky organization.spec.ts test for i.cal.com (#20421)
* Fix flakiness in checjly

* Fix isEmbed check
2025-03-28 04:50:09 +00:00
Joe Au-Yeung c0aa97178c feat: Prevent watchlisted entities from creating bookings (#19107)
* Add `searchForAllBlockedRecords` to watchlist repository

* Create `checkIfUsersAreBlocked`

* Call `checkIfUsersAreBlocked` in `loadAndValidateUsers`

* Draft test

* Add tests

* Type fix

* Type fix
2025-03-28 01:11:10 +00:00
Peer Richelsen fc8ff2d4ae nit changes to referral embed (#20419) 2025-03-27 23:41:16 +00:00
Anik Dhabal Babu c1a6ccc260 fix: system booking field is not tranlate in calndar event (#20410) 2025-03-27 19:27:31 +00:00
Morgan b17f47236e fix: atoms build inferred types (#20409)
* fix: atoms build inferred types

* type button cva

* chore: refactor react imports

* chore: refactor react imports toast
2025-03-27 19:00:45 +00:00
GitHub Actions 236fd967ad chore: release v5.1.10
Post release / release (push) Canceled after 0s
2025-03-27 18:51:29 +00:00
Joe Au-Yeung 7f5534b340 fix: Salesforce ownership query (#20405)
* Fix query

* Add test
2025-03-27 16:08:09 +00:00
Alex van Andel 50bacf6ea2 Add 'await' to searchParams, parse failed (#20407) 2025-03-27 16:07:52 +00:00
Hariom Balhara 0d1adcca58 Ensure upId in token for Google Signin which is used by getServerSession (#20399) 2025-03-27 15:42:09 +00:00
Somay ChauhanandMorgan 81962efb2e fix: Restore event name field in Advanced Event Settings for Atom (#20394)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-03-27 17:22:12 +02:00
sean-brydonandKeith Williams da47168941 fix: Stop throwing 202 for stripe handler (#20397)
* dont throw - log error

* invoice paid

* update to warn

* Update packages/features/ee/billing/api/webhook/_invoice.paid.ts

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-27 15:06:15 +00:00
Hariom Balharaandamrit 2aa583bb6d feat: Toggle to hide Team Profile link on Booker (#20369)
* hide team profile link on booker

* fix ts errors

---------

Co-authored-by: amrit <iamamrit27@gmail.com>
2025-03-27 15:00:35 +00:00
Eunjae Lee ea250febbc fix: style DataTableSelectionBar (#20403) 2025-03-27 14:29:45 +00:00
Keith Williams f062014952 chore: Use fixed mintlify version for builds (#20402) 2025-03-27 14:11:50 +00:00
Eunjae LeeandAlex van Andel 88961a1c67 fix: attribute filters on organization member list (#20380)
* fix: attribute filters on organization member list

* fix total count

* add tests

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-03-27 14:36:47 +01:00
MorganandhbjORbj cd952dc93c chore: decouple ui dialog from AppRouter and Atoms (#20088)
* chore: decouple ui dialog from AppRouter and Atoms

* fix type e2e test

* fix: import dialog from @calcom/ui/components/dialog

* fix: remove log

* fix unit test

* fix

* fix and move data-override-list test to features package

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-27 09:24:42 +02:00
Hariom Balhara b0ed1fd5e6 dont install the scripts on booking pages (#20373) 2025-03-27 11:12:28 +05:30
Keith WilliamsandhbjORbj be883aac02 perf: Remove ssrInit from layout and event-types (#20368)
* perf: Remove ssrInit from layout and event-types

* remove ssg file since not used

* refator trpc usage in app router

* fix

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-26 22:04:52 -03:00
Alex van Andel 2ab4e8b40a fix: support Stripe subscription items (#20389)
* fix: Implement backwards compatible stripe change to customer.subscription.deleted

* Further handling, including price -> plan change
2025-03-26 23:24:38 +00:00
sean-brydonandPeer Richelsen 6bd11009a8 allow promotion codes (#20387)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-03-26 22:51:10 +00:00
099c534daa feat: dub refer embed (#20258)
* feat: dub refer embed

* review fix

* review fix

* @dub/embed-core seems not needed

* @dub/embed-core can stay in web

* move refer page out of main-nav

* update yarn lock

* use loading.tsx

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-26 21:06:21 +00:00
Udit Takkar 7aa1d8035c fix: update error handling in validateBookingTimeIsNotOutOfBounds function to use HttpError (#20386) 2025-03-26 17:24:33 +00:00
Udit Takkar 596e9b4e5a fix: circular dependencies (#20378)
* fix: circular dependencies

* fix: type err
2025-03-26 12:42:40 -04:00
Lauris Skraucis d04f10e7d1 refactor: platform - remove host and attendee automatic no show webhook trigger (#20385)
* docs: connect calendar redir and auto accept team creator

* refactor: platform hide automatic no show webhook triggers
2025-03-26 21:55:49 +05:30
0fec74a7fe fix: #11378 (#20384)
* fix:#11378

* fix: #11378

* final-changes

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Tushar <tusharbhatt0135@gmail.com>
2025-03-26 14:51:39 +00:00
Eunjae Lee d927662dd7 fix: relocate action buttons on org member list (#20336)
* fix: relocate filter segment buttons on org member list

* implement toolbarContainerRef

* adjust location

* reorganize the hierarchy of DataTableProvider

* rename toolbar container to cta container

* move clear button to toolbar right
2025-03-26 14:18:39 +00:00
sean-brydon e1c001baff adds scroll indicators and sticks to one line (#20383) 2025-03-26 14:15:54 +00:00
Joe Au-Yeung 122af4538d fix: Add assignment reason for round robin reassignments (#19199)
* Add `RR_REASSIGNED` to schema

* Handle recording reassigned manual vs rr

* Pass manual assignment prop to `AssignmentReasonRecorder`

* Add log to determine who is doing the reassignment

* RR reassign - add initializing log

* Record assignment reason when reassigning via rr

* Add `reassignedById` in trpc endpoint

* Add `reassignedById` param in api v2 endpoint

* Migration to add `RR_REASSIGNED`
2025-03-26 10:39:16 -03:00
GitHub Actions e215786afb chore: release v5.1.9
Post release / release (push) Canceled after 0s
2025-03-26 11:49:54 +00:00
Eunjae Lee f699fd1732 fix: add UID filter to /insights/routing (#20356) 2025-03-26 09:52:40 +00:00
Hariom Balhara a9ddea9184 fix crash in vercel domain creation flow (#20281) 2025-03-26 13:05:55 +05:30
Gaurav VermaandTushar Bhatt d4edd30a07 fix: layout of dialog (edit location) on bookings page (#20268)
* refactor: streamline DialogFooter structure in EditLocationDialog component

* refactor: enhance form handling and structure in EditLocationDialog component

* refactor: update icon styling and layout in EditLocationDialog component

* revert: size to h,w

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-26 09:35:05 +05:30
Calcom BotandLingo.dev f98a3c9b69 feat: update translations via @lingodotdev (#20367)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-26 02:34:39 +00:00
Benny Joo dcc079773b fix: type error in payment page (#20366) 2025-03-26 01:48:27 +00:00
Omar López 2a2e2871a8 ci: add QA status badge (#20363) 2025-03-25 21:28:42 +00:00
GitHub Actions 0befc6431f chore: release v5.1.8
Post release / release (push) Canceled after 0s
2025-03-25 21:10:18 +00:00
Benny Joo 35c363f75c fix: better translations for korean (#20362) 2025-03-25 19:13:49 +00:00
Benny Joo ad6d5a01f8 fix: some translations got wrong closing tags (#20360) 2025-03-25 14:59:51 -04:00
0xIvan 11ae957d97 Fix quickstart link (#20355) 2025-03-25 18:27:24 +00:00
Alex van Andel c21094ab0a fix: Buffers when combined with seated events (#20233) 2025-03-25 18:17:25 +00:00
Benny Joo 9b952fd922 chore: Set daily video testing meeting link to env var (#20359) 2025-03-25 18:17:00 +00:00
Ayush Agarwal ecd85cd15a feat: enhance onboarding steps with loading state and button component (#20350)
- Added loading state to ConnectedCalendars and ConnectedVideoStep components.
- Replaced native button elements with a custom Button component for consistency.
- Updated SetupAvailability to use async mutation methods.
- Integrated loading state into the onboarding view for smoother transitions between steps.
2025-03-25 18:13:09 +00:00
Alex van Andel 0772e1d168 fix: skip when browsing month has no available slots (#19626)
* fix: skip when browsing month has no available slots

* Fix zustand error + add wip tests

* Fix issue with using brows
ingDate too early

* Use slots instead of schedule return types to remove hard coupling to tRPC

* Some await fixes + use type definition from extended core feature

* Move useNextMonth to custom hook

* Add unit tests to cover the next month scenario

* Use changed interface, schedule -> slots,isLoading

* Return no-op until ready
2025-03-25 17:28:50 +00:00
Benny Joo d131d58b19 feat: upgrade @daily libs (#20348)
* upgrade @daily libs

* install peer dep

* add code for local testing
2025-03-25 17:27:00 +00:00
Lauris SkraucisandRajiv Sahal 8a18e167e9 fix: remove oAuth clientId from email when rescheduling platform booking (#20354)
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2025-03-25 13:18:29 +00:00
Benny Joo c7c2c03f6a chore: move ServerTrans to packages/lib (#20353)
* chore: move ServerTrans to packages/lib

* remove file not meant for this PR
2025-03-25 10:23:42 +00:00
Morgan a183b31269 refactor: atoms dependency on web through AdditionalCalendarSelector (#20352) 2025-03-25 09:09:57 +00:00
Benny Joo bec8bf2eaa chore: improve e2e test suite for i18n routing (#20349) 2025-03-25 13:55:29 +05:30
Anik Dhabal Babu 1789aef7d3 fix: organizer req email v2 issue (#20314) 2025-03-25 05:10:42 +00:00
Anik Dhabal Babu 7b9dabfa17 fix: disable guest is not updating onchange (#20340) 2025-03-25 04:51:13 +00:00
Anik Dhabal Babu 3ecc9319c7 fix: Introduce a new prop in the session to extract the profile username when inside an org (#18979)
* fix: flaky test organization.spec

* fix: use profile.username

* update

* update

* update
2025-03-25 09:46:00 +05:30
GitHub Actions 214528ffa8 chore: release v5.1.7
Post release / release (push) Canceled after 0s
2025-03-25 00:23:18 +00:00
Keith WilliamsandAlex van Andel 7f79779bf8 chore: Upgrade jackson (#20107)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-03-25 00:18:26 +00:00
GitHub Actions ed0f8fe0e7 chore: release v5.1.6
Post release / release (push) Canceled after 0s
2025-03-24 23:23:16 +00:00
Benny Joo 273fc544e7 feat: rewrite all routes starting with supported lang code (#20346)
* feat: rewrite all routes starting with matching locale code

* update e2e test
2025-03-24 23:15:39 +00:00
Joe Au-Yeung f24e8d12f0 feat: Salesforce - handle picklist field type (#20342)
* Add picklist to `SalesforceFieldType` enum

* Add picklist value to Salesforce settings

* Add `getPicklistFieldValue` method

* Find picklist option based on active and value
2025-03-24 23:05:30 +00:00
Omar López 19712f62e3 feat: improved observability on api v1 and webapp (#20302)
* feat: associate errors to users on api v1

* Added webapp

* Update layout.tsx

* Update captureUserId.ts
2025-03-24 23:01:20 +00:00
Benny JooandAnik Dhabal Babu 0ec701f9e6 fix: rewrite /en/* paths to /* (#20331)
* fix: add back i18n config to next.config.js

* add back localeDetection

* rewrite /en/* paths to /

* add e2e test

* add comment

* add test

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-24 16:50:55 -03:00
sean-brydon e4cfaa56f0 no throw 202 (#20339) 2025-03-24 16:48:38 +00:00
c7641f852f feat: Added org username to admin users view (#20254)
* feat : Added org username to admin users view

* styling tweaks (#20215)

* minor-changes

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Tushar <tusharbhatt0135@gmail.com>
2025-03-24 15:03:58 +00:00
Ayanava Karmakar e2597fbcd2 docs: fix invalid JSX in code snippets in platform/guides/booking-redirects (#20321) 2025-03-24 14:32:02 +00:00
Zayd Krunz ce88018ecc Update aws.mdx (#20330) 2025-03-24 14:31:01 +00:00
Vijay 0eb4e6c7d4 explicitly specify default visibility value till initializes (#20312) 2025-03-24 14:25:56 +00:00
Keith Williams 10c9c63ad8 revert: "fix: lint (#20325)" (#20337)
This reverts commit 1dc63e0df7.
2025-03-24 11:02:04 -03:00
Omar López a18defaa36 fix: increase memory requirement for building atoms (#20306) 2025-03-24 10:50:35 -03:00
GitHub Actions f9ee907245 chore: release v5.1.5
Post release / release (push) Canceled after 0s
2025-03-24 00:23:17 +00:00
Calcom BotandLingo.dev eac6b12e91 feat: update translations via @lingodotdev (#20320)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-23 23:07:36 +00:00
Anik Dhabal Babu 1dc63e0df7 fix: lint (#20325) 2025-03-23 23:03:25 +00:00
Keith Williams 96cd3f7b2f chore: Upgrade @sentry/nextjs to 9.8.0 (#20324)
* chore: Upgrade Sentry

* Use ^9.8.0
2025-03-23 15:24:59 -03:00
Keith Williams 7943a83e7d fix: lint errors (#20323) 2025-03-23 14:38:53 +00:00
Anik Dhabal Babu 01c16ad150 fix: calender setting skeleton (#20317) 2025-03-23 08:23:28 +00:00
Ayanava Karmakar 42807481c1 fix(docs/platform/guides) - use correct variable in reschedule booker code snippet (#20316) 2025-03-23 04:51:05 +00:00
Keith Williams 032c41fefd chore: Upgrade Next to 15.2.3 (#20315) 2025-03-22 15:07:57 +00:00
Vijay 41041226b4 wait in advance and avoid race condition (#20311) 2025-03-22 14:56:01 +05:30
Hariom Balhara 56ab4e8415 Add salesforce query validation in integration tests (#20209) 2025-03-21 23:06:41 +00:00
Calcom BotandLingo.dev 8ee6d2befa feat: update translations via @lingodotdev (#20307)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-21 20:10:34 +00:00
Omar López 9cd133ff10 Auto builds icons before dev and build scripts (#20304) 2025-03-21 19:59:00 +00:00
GitHub Actions 6df473c5fb chore: release v5.1.4
Post release / release (push) Canceled after 0s
2025-03-21 19:14:23 +00:00
Benny Joo 4606fc6ace chore: Remove HeadSeo everywhere (#20272) 2025-03-21 14:24:53 -04:00
78ddf71823 perf: Move OOO-related tRPC procedures to new routers (#20136)
* perf: Move OOO-related tRPC procedures to new routers

* Moved test file

* push fix for import path in tests

* fix trpc response url that we wait for

---------

Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-03-21 09:03:10 -07:00
Morgan 15cb687307 chore: ensure default calendars for delegated users (#20300)
* chore: ensure default calendars for delegated users

* fixup! chore: ensure default calendars for delegated users
2025-03-21 15:41:04 +00:00
Lauris Skraucis c0ecfcd910 docs: v2 team event schedulingType (#20289)
* refactor: input scheduling type

* refactor: output scheduling type
2025-03-21 13:25:59 +00:00
Lauris Skraucis 2e86f8404f fix: booker atom booking with split name (#20298) 2025-03-21 13:25:32 +00:00
Morgan b94b16316f fix: i18n failed to fetch translations results in api v2 crash (#20294)
* fix: i18n failed to fetch translations

* fixup! fix: i18n failed to fetch translations
2025-03-21 14:22:38 +02:00
Tushar Bhatt f80b780aae fix: removing unnecessary styles (#20284)
* fix: removing unnecessary styles

* add padding

* padding

* padding
2025-03-21 16:36:21 +05:30
Pranav Patil 00dc467f25 fix: Padding issue in text fields (#20287) 2025-03-21 09:03:54 +00:00
Morgan d644c09bac fix: missing Prisma type in libraries (#20285) 2025-03-21 08:50:14 +00:00
Vijay 5d9c7bfde9 place correct actions before await (#20266) 2025-03-21 09:39:21 +05:30
Benny Joo cfd1c5dfe3 show-skeleton-only-if-cached-data-do-not-exist (#20275) 2025-03-21 09:38:58 +05:30
amrit d7e5a8e588 fix: update SkeletonLoaderTeamList component layout (#20263) 2025-03-21 02:21:54 +00:00
Benny Joo 3b992fa9bd chore: missing awaits for params/search params in two pages (#20276) 2025-03-21 02:19:15 +00:00
8ad442f2be feat: Upgrade to Next 15 (#18834)
* wip

* update layoutHOC

* wip

* remove app router related code no longer used

* remove more

* await cookies, headers, params, serachparams

* update yarn.lock

* await cookies, headers, params, serachparams more

* update yarn lock again

* downgrade next-auth to 4.22.1

* update yarn lock

* fix

* update yarn lock

* fix type checks

* update yarn lock

* await headers and cookies

* restore pages folder

* restore yarn.lock

* update yarn.lock

* await headers and cookies

* remove

* await params in API routes

* updates

* restore next.config.js

* remove i18n from next.config.js

* Fixed tests

* Fixed types

* Removed duplicate favicon.ico

* Fixing more types

* ImageResponse moved to next/og

* Fixed prisma import issues

* dynamic import for @ewsjs/xhr

* remove deasync dep

* dynamic import for @tryvital/vital-node

* fix type checks

* add back turbopack command

* Type fix

* Removed unneeded file

* fix turbopack relative path errors

* add comments

* remove unneeded code

* Fixed build errors

* await apis

* use Promise<Params> type in defaultResponderForAppDir util

* refactor scim api route

* fix type checks

* separate app-routing.config into client-config and server-config

* wip

* refactor routing forms components

* revert unneeded changes for easier review

* fix

* fix

* use CustomTrans

* fix type

* fix unit tests

* fix type error

* fix build error

* fix build error

* fix build error

* fix warnings

* fix warnings

* upgrade @tremor/react and tailwindcss

* numCols -> numItems

* fix forgot-password e2e test

* fix 1 more e2e test

* fix login e2e test

* fix e2e tests

* fix e2e tests

* clean up

* remove error

* use tremor/react 2.11.0

* fix

* rename CustomTrans to ServerTrans

* address comment

* fix test

* fix ServerTrans

* fix

* fix type

* fix ServerTrans usages 1

* fix translations

* fix type checks

* fix type checks

* link styling

* fix

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-20 21:30:51 -03:00
778 changed files with 21349 additions and 9213 deletions
+16 -10
View File
@@ -117,6 +117,11 @@ NEXT_PUBLIC_PLAIN_CHAT_ID=
PLAIN_CHAT_HMAC_SECRET_KEY=
NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS=
# Dub Config
DUB_API_KEY=
NEXT_PUBLIC_DUB_PROGRAM_ID=
# Zendesk Config
NEXT_PUBLIC_ZENDESK_KEY=
@@ -357,13 +362,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
# ***********************************************************************************************************
# provide a value between 0 and 100 to ensure the percentage of traffic
# redirected from the legacy to the future pages
AB_TEST_BUCKET_PROBABILITY=50
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
APP_ROUTER_APPS_ENABLED=0
APP_ROUTER_TEAM_ENABLED=0
# disable setry server source maps
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
@@ -411,8 +409,12 @@ CAL_VIDEO_BUCKET_NAME=
CAL_VIDEO_BUCKET_REGION=
CAL_VIDEO_ASSUME_ROLE_ARN=
# Used to use Replexica SDK, a tool for real-time AI-powered localization
REPLEXICA_API_KEY=
# For local testing of Daily Video
# Set this to the link of a working Daily Video meeting link
CAL_VIDEO_MEETING_LINK_FOR_TESTING=
# Used to use lingo.dev SDK, a tool for real-time AI-powered localization
LINGO_DOT_DEV_API_KEY=
# Comma-separated list of DSyncData.directoryId to log SCIM API requests for. It can be enabled temporarily for debugging the requests being sent to SCIM server.
DIRECTORY_IDS_TO_LOG=
@@ -432,4 +434,8 @@ NEXT_PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS=
# Used to invalidate available slots when navigating to booking form
NEXT_PUBLIC_INVALIDATE_AVAILABLE_SLOTS_ON_BOOKING_FORM=0
# Used to enable quick availability checks for x% of all visitors
NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT=10
NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT=10
# Deprecated. Use NEXT_PUBLIC_BODY_SCRIPTS instead. NEXT_PUBLIC_HEAD_SCRIPTS are now injected into the body only.
NEXT_PUBLIC_HEAD_SCRIPTS=
NEXT_PUBLIC_BODY_SCRIPTS=
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
working-directory: docs
run: |
export NODE_OPTIONS="--max_old_space_size=8192"
npm install -g mintlify
npm install -g mintlify@4.0.436
mintlify dev &
sleep 5 # Let it run for 5 seconds
kill $!
+1 -1
View File
@@ -17,7 +17,7 @@ env:
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NODE_OPTIONS: --max-old-space-size=29000
REDIS_URL: "redis://localhost:6379"
REPLEXICA_API_KEY: ${{ secrets.CI_REPLEXICA_API_KEY }}
LINGO_DOT_DEV_API_KEY: ${{ secrets.CI_LINGO_DOT_DEV_API_KEY }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
+1 -1
View File
@@ -21,5 +21,5 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
with:
api-key: ${{ secrets.CI_REPLEXICA_API_KEY }}
api-key: ${{ secrets.CI_LINGO_DOT_DEV_API_KEY }}
pull-request: true
+1 -1
View File
@@ -2,7 +2,7 @@ name: Unit
on:
workflow_call:
env:
REPLEXICA_API_KEY: ${{ secrets.CI_REPLEXICA_API_KEY }}
LINGO_DOT_DEV_API_KEY: ${{ secrets.CI_LINGO_DOT_DEV_API_KEY }}
permissions:
contents: read
jobs:
View File
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+7
View File
@@ -0,0 +1,7 @@
undecided:
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+7
View File
@@ -0,0 +1,7 @@
undecided:
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
View File
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+1
View File
@@ -24,6 +24,7 @@
<p align="center">
<a href="https://www.producthunt.com/products/cal-com"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
<img src="https://api.checklyhq.com/v1/badges/groups/1120718?style=flat&theme=default" alt="Checkly QA">
<a href="https://status.cal.com"><img height="20px" src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
<a href="https://news.ycombinator.com/item?id=34507672"><img src="https://img.shields.io/badge/Hacker%20News-%231-%23FF6600" alt="Hacker News"></a>
+14 -14
View File
@@ -1,4 +1,3 @@
import type { Page } from "@playwright/test";
import { test, expect } from "@playwright/test";
test.describe("Org", () => {
@@ -8,14 +7,16 @@ test.describe("Org", () => {
const response = await page.goto("https://i.cal.com/embed");
expect(response?.status()).toBe(200);
await page.screenshot({ path: "screenshot.jpg" });
await expectPageToBeRenderedWithEmbedSsr(page);
const body = await response?.text();
await expectPageToBeRenderedWithEmbedSsr(body);
});
test("Org User(Rick) Page should be embeddable", async ({ page }) => {
const response = await page.goto("https://i.cal.com/team-rick/embed");
expect(response?.status()).toBe(200);
await expect(page.locator("text=Used by Checkly")).toBeVisible();
await expectPageToBeRenderedWithEmbedSsr(page);
const body = await response?.text();
await expectPageToBeRenderedWithEmbedSsr(body);
});
test("Org User Event(/team-rick/test-event) Page should be embeddable", async ({ page }) => {
@@ -23,14 +24,16 @@ test.describe("Org", () => {
expect(response?.status()).toBe(200);
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
await expectPageToBeRenderedWithEmbedSsr(page);
const body = await response?.text();
await expectPageToBeRenderedWithEmbedSsr(body);
});
test("Org Team Profile(/sales) page should be embeddable", async ({ page }) => {
const response = await page.goto("https://i.cal.com/sales/embed");
expect(response?.status()).toBe(200);
await expect(page.locator("text=Cal.com Sales")).toBeVisible();
await expectPageToBeRenderedWithEmbedSsr(page);
const body = await response?.text();
await expectPageToBeRenderedWithEmbedSsr(body);
});
test("Org Team Event page(/sales/hippa) should be embeddable", async ({ page }) => {
@@ -38,7 +41,8 @@ test.describe("Org", () => {
expect(response?.status()).toBe(200);
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
await expectPageToBeRenderedWithEmbedSsr(page);
const body = await response?.text();
await expectPageToBeRenderedWithEmbedSsr(body);
});
});
@@ -68,7 +72,8 @@ test.describe("Org", () => {
test("Organization Homepage - Has Engineering and Marketing Teams", async ({ page }) => {
const response = await page.goto("https://i.cal.com");
expect(response?.status()).toBe(200);
await expect(page.locator("text=Cal.com")).toBeVisible();
// Somehow there are two Cal.com text momentarily, but shouldn't be the concern of this check
await expect(page.locator("text=Cal.com").first()).toBeVisible();
await expect(page.locator("text=Engineering")).toBeVisible();
await expect(page.locator("text=Marketing")).toBeVisible();
});
@@ -95,11 +100,6 @@ test.describe("Org", () => {
});
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
async function expectPageToBeRenderedWithEmbedSsr(page: Page) {
expect(
await page.evaluate(() => {
//@ts-expect-error - __NEXT_DATA__ is a global variable defined by Next.js
return window.__NEXT_DATA__.props.pageProps.isEmbed;
})
).toBe(true);
async function expectPageToBeRenderedWithEmbedSsr(responseText: string | undefined) {
expect(responseText).toContain('\\"isEmbed\\":true');
}
+8
View File
@@ -0,0 +1,8 @@
import { setUser as SentrySetUser } from "@sentry/nextjs";
import type { NextMiddleware } from "next-api-middleware";
export const captureUserId: NextMiddleware = async (req, res, next) => {
if (req.userId) SentrySetUser({ id: req.userId });
await next();
};
+7 -4
View File
@@ -2,15 +2,16 @@ import { label } from "next-api-middleware";
import { addRequestId } from "./addRequestid";
import { captureErrors } from "./captureErrors";
import { captureUserId } from "./captureUserId";
import { checkIsInMaintenanceMode } from "./checkIsInMaintenanceMode";
import { extendRequest } from "./extendRequest";
import {
HTTP_POST,
HTTP_DELETE,
HTTP_PATCH,
HTTP_GET,
HTTP_GET_OR_POST,
HTTP_GET_DELETE_PATCH,
HTTP_GET_OR_POST,
HTTP_PATCH,
HTTP_POST,
} from "./httpMethods";
import { rateLimitApiKey } from "./rateLimitApiKey";
import { verifyApiKey } from "./verifyApiKey";
@@ -31,6 +32,7 @@ const middleware = {
extendRequest,
pagination: withPagination,
captureErrors,
captureUserId,
verifyCredentialSyncEnabled,
};
@@ -44,8 +46,9 @@ const middlewareOrder = [
"verifyApiKey",
"rateLimitApiKey",
"addRequestId",
"captureUserId",
] as Middleware[]; // <-- Provide a list of middleware to call automatically
const withMiddleware = label(middleware, middlewareOrder);
export { withMiddleware, middleware, middlewareOrder };
export { middleware, middlewareOrder, withMiddleware };
+1 -1
View File
@@ -33,7 +33,7 @@
"@sentry/nextjs": "^8.8.0",
"bcryptjs": "^2.4.3",
"memory-cache": "^0.2.0",
"next": "^13.5.6",
"next": "^13.5.9",
"next-api-middleware": "^1.0.1",
"next-axiom": "^0.17.0",
"next-swagger-doc": "^0.3.6",
@@ -79,9 +79,10 @@ async function handler(req: NextApiRequest) {
allRemainingBookings: req.query.allRemainingBookings === "true",
});
// Normalizing for universal handler
req.body = { id, allRemainingBookings, cancellationReason, cancelledBy };
return await handleCancelBooking(req);
return await handleCancelBooking({
bookingData: { id, allRemainingBookings, cancellationReason, cancelledBy },
userId: req.userId,
});
}
export default defaultResponder(handler);
+1 -1
View File
@@ -1,8 +1,8 @@
import type { NextApiRequest } from "next";
import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
import stripe from "@calcom/app-store/stripepayment/lib/server";
import { getDubCustomer } from "@calcom/features/auth/lib/dub";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.152",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.164",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -351,6 +351,64 @@ describe("Bookings Endpoints 2024-04-15", () => {
});
});
it("should create a booking with split name", async () => {
const bookingStart = "2040-05-21T11:30:00.000Z";
const bookingEnd = "2040-05-21T12:30:00.000Z";
const bookingEventTypeId = eventTypeId;
const bookingTimeZone = "Europe/London";
const bookingLanguage = "en";
const bookingHashedLink = "";
const bookingMetadata = {
timeFormat: "12",
meetingType: "organizer-phone",
};
const bookingResponses = {
name: { firstName: "John", lastName: "Doe" },
email: "tester@example.com",
location: {
value: "link",
optionValue: "",
},
notes: "test",
};
const body: CreateBookingInput_2024_04_15 = {
start: bookingStart,
end: bookingEnd,
eventTypeId: bookingEventTypeId,
timeZone: bookingTimeZone,
language: bookingLanguage,
metadata: bookingMetadata,
hashedLink: bookingHashedLink,
responses: bookingResponses,
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.userPrimaryEmail).toBeDefined();
expect(responseBody.data.userPrimaryEmail).toEqual(userEmail);
expect(responseBody.data.id).toBeDefined();
expect(responseBody.data.uid).toBeDefined();
expect(responseBody.data.startTime).toEqual(bookingStart);
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
expect(responseBody.data.metadata).toEqual(bookingMetadata);
expect(responseBody.data.responses).toEqual({
...bookingResponses,
name: `${bookingResponses.name.firstName} ${bookingResponses.name.lastName}`,
});
createdBooking = responseBody.data;
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
@@ -214,9 +214,16 @@ export class BookingsController_2024_04_15 {
if (bookingUid) {
try {
req.body.uid = bookingUid;
const res = await handleCancelBooking(
await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed)
);
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
const res = await handleCancelBooking({
bookingData: bookingRequest.body,
userId: bookingRequest.userId,
arePlatformEmailsEnabled: bookingRequest.arePlatformEmailsEnabled,
platformClientId: bookingRequest.platformClientId,
platformCancelUrl: bookingRequest.platformCancelUrl,
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
});
if (!res.onlyRemovedAttendee) {
void (await this.billingService.cancelUsageByBookingUid(res.bookingUid));
}
@@ -12,9 +12,42 @@ import {
isEmail,
Validate,
} from "class-validator";
import { ValidationOptions, registerDecorator } from "class-validator";
import { RESCHEDULED_BY_DOCS } from "@calcom/platform-types";
type BookingName = { firstName: string; lastName: string };
function ValidateBookingName(validationOptions?: ValidationOptions) {
return function (target: object, propertyName: string) {
registerDecorator({
name: "validateBookingName",
target: target.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any) {
if (typeof value === "string") {
return value.trim().length > 0;
}
if (typeof value === "object" && value !== null) {
return (
typeof value.firstName === "string" &&
typeof value.lastName === "string" &&
value.firstName.trim().length > 0 &&
value.lastName.trim().length > 0
);
}
return false;
},
defaultMessage(): string {
return "Name must be either a non-empty string or an object with non-empty firstName and lastName";
},
},
});
};
}
class Location {
@IsString()
@ApiProperty()
@@ -26,9 +59,14 @@ class Location {
}
class Response {
@IsString()
@ApiProperty()
name!: string;
@ApiProperty({
oneOf: [
{ type: "string" },
{ type: "object", properties: { firstName: { type: "string" }, lastName: { type: "string" } } },
],
})
@ValidateBookingName()
name!: string | BookingName;
@Validate((value: string) => !value || isEmail(value), {
message: "Invalid response email",
@@ -7,6 +7,7 @@ import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outpu
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -155,12 +156,7 @@ export class BookingsController_2024_08_13 {
@Get("/")
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Permissions([BOOKING_READ])
@ApiOperation({ summary: "Get all bookings" })
async getBookings(
@@ -245,13 +241,10 @@ export class BookingsController_2024_08_13 {
@HttpCode(HttpStatus.OK)
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard, BookingUidGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Mark a booking absence - provided authorization header refers to owner of the booking.",
})
@ApiOperation({ summary: "Mark a booking absence" })
async markNoShow(
@Param("bookingUid") bookingUid: string,
@Body() body: MarkAbsentBookingInput_2024_08_13,
@@ -269,13 +262,11 @@ export class BookingsController_2024_08_13 {
@HttpCode(HttpStatus.OK)
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard, BookingUidGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary:
"Automatically reassign booking to a new host automatically - provided authorization header refers to person who reassigned the booking.",
})
@ApiOperation({ summary: "Automatically reassign booking to a new host" })
async reassignBooking(
@Param("bookingUid") bookingUid: string,
@GetUser() user: UserWithProfile
@@ -292,13 +283,11 @@ export class BookingsController_2024_08_13 {
@HttpCode(HttpStatus.OK)
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard, BookingUidGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary:
"Reassign a booking to a specific user specified by the :userId - provided authorization header refers to person who reassigned the booking.",
})
@ApiOperation({ summary: "Reassign a booking to a specific user" })
async reassignBookingToUser(
@Param("bookingUid") bookingUid: string,
@Param("userId") userId: number,
@@ -322,13 +311,11 @@ export class BookingsController_2024_08_13 {
@HttpCode(HttpStatus.OK)
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard, BookingUidGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary:
"Confirm booking that requires a confirmation - provided authorization header refers to owner of the booking.",
})
@ApiOperation({ summary: "Confirm booking that requires a confirmation" })
async confirmBooking(
@Param("bookingUid") bookingUid: string,
@GetUser() user: UserWithProfile
@@ -345,13 +332,11 @@ export class BookingsController_2024_08_13 {
@HttpCode(HttpStatus.OK)
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard, BookingUidGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary:
"Decline booking that requires a confirmation - provided authorization header refers to owner of the booking.",
})
@ApiOperation({ summary: "Decline booking that requires a confirmation" })
async declineBooking(
@Param("bookingUid") bookingUid: string,
@Body() body: DeclineBookingInput_2024_08_13,
@@ -368,6 +353,7 @@ export class BookingsController_2024_08_13 {
@Get("/:bookingUid/calendar-links")
@UseGuards(ApiAuthGuard, BookingUidGuard)
@Permissions([BOOKING_READ])
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Get 'Add to Calendar' links for a booking",
description:
@@ -344,7 +344,16 @@ export class BookingsService_2024_08_13 {
}
const bookingRequest = await this.inputService.createCancelBookingRequest(request, bookingUid, body);
const res = await handleCancelBooking(bookingRequest);
const res = await handleCancelBooking({
bookingData: bookingRequest.body,
userId: bookingRequest.userId,
arePlatformEmailsEnabled: bookingRequest.arePlatformEmailsEnabled,
platformClientId: bookingRequest.platformClientId,
platformCancelUrl: bookingRequest.platformCancelUrl,
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
});
if (!res.onlyRemovedAttendee) {
await this.billingService.cancelUsageByBookingUid(res.bookingUid);
}
@@ -448,6 +457,7 @@ export class BookingsService_2024_08_13 {
orgId: profile?.organizationId || null,
emailsEnabled,
platformClientParams,
reassignedById: requestUser.id,
});
const reassigned = await this.bookingsRepository.getByUidWithUser(bookingUid);
@@ -14,6 +14,7 @@ import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service";
import { OutlookService } from "@/ee/calendars/services/outlook.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -34,7 +35,7 @@ import {
Post,
Body,
} from "@nestjs/common";
import { ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { plainToClass } from "class-transformer";
import { Request } from "express";
@@ -68,6 +69,7 @@ export class CalendarsController {
@Post("/ics-feed/save")
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Save an ICS feed" })
async createIcsFeed(
@GetUser("id") userId: number,
@@ -79,12 +81,14 @@ export class CalendarsController {
@Get("/ics-feed/check")
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Check an ICS feed" })
async checkIcsFeed(@GetUser("id") userId: number): Promise<ApiResponse> {
return await this.icsFeedService.check(userId);
}
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Get("/busy-times")
@ApiOperation({
summary: "Get busy times",
@@ -113,6 +117,7 @@ export class CalendarsController {
@Get("/")
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Get all calendars" })
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
const calendars = await this.calendarsService.getCalendars(userId);
@@ -129,9 +134,16 @@ export class CalendarsController {
name: "calendar",
})
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Get("/:calendar/connect")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Get oAuth connect URL" })
@ApiQuery({
name: "redir",
required: false,
type: String,
description: "Redirect URL after successful calendar authorization.",
})
async redirect(
@Req() req: Request,
@Headers("Authorization") authorization: string,
@@ -193,6 +205,7 @@ export class CalendarsController {
name: "calendar",
})
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Post("/:calendar/credentials")
@ApiOperation({ summary: "Sync credentials" })
async syncCredentials(
@@ -221,6 +234,7 @@ export class CalendarsController {
@Get("/:calendar/check")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard, PermissionsGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Permissions([APPS_READ])
@ApiOperation({ summary: "Check a calendar connection" })
async check(@GetUser("id") userId: number, @Param("calendar") calendar: string): Promise<ApiResponse> {
@@ -245,6 +259,7 @@ export class CalendarsController {
name: "calendar",
})
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Post("/:calendar/disconnect")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Disconnect a calendar" })
@@ -0,0 +1,33 @@
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { Process, Processor } from "@nestjs/bull";
import { Logger } from "@nestjs/common";
import { Job } from "bull";
export const DEFAULT_CALENDARS_JOB = "default_calendars_job";
export const CALENDARS_QUEUE = "calendars";
export type DefaultCalendarsJobDataType = {
userId: number;
};
@Processor(CALENDARS_QUEUE)
export class CalendarsProcessor {
private readonly logger = new Logger(CalendarsProcessor.name);
constructor(public readonly calendarsService: CalendarsService) {}
@Process(DEFAULT_CALENDARS_JOB)
async handleEnsureDefaultCalendars(job: Job<DefaultCalendarsJobDataType>) {
const { userId } = job.data;
try {
// getCalendars calls getConnectedDestinationCalendarsAndEnsureDefaultsInDb from platform libraries
// which gets the calendars from third party providers and ensure default calendars are set in DB
await this.calendarsService.getCalendars(userId);
} catch (err) {
this.logger.error(`Failed to load default calendars of user with id: ${userId}`, {
userId,
err,
});
}
return;
}
}
@@ -7,6 +7,7 @@ import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_202
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -67,12 +68,7 @@ export class EventTypesController_2024_06_14 {
@Post("/")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Create an event type" })
async createEventType(
@Body() body: CreateEventTypeInput_2024_06_14,
@@ -94,12 +90,7 @@ export class EventTypesController_2024_06_14 {
@Get("/:eventTypeId")
@Permissions([EVENT_TYPE_READ])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Get an event type" })
async getEventTypeById(
@Param("eventTypeId") eventTypeId: string,
@@ -149,12 +140,7 @@ export class EventTypesController_2024_06_14 {
@Patch("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Update an event type" })
async updateEventType(
@@ -179,12 +165,7 @@ export class EventTypesController_2024_06_14 {
@Delete("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Delete an event type" })
async deleteEventType(
@Param("eventTypeId") eventTypeId: number,
+3 -1
View File
@@ -2,6 +2,7 @@ import { GetMeOutput } from "@/ee/me/outputs/get-me.output";
import { UpdateMeOutput } from "@/ee/me/outputs/update-me.output";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -10,7 +11,7 @@ import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-us
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 { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { PROFILE_READ, PROFILE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { userSchemaResponse } from "@calcom/platform-types";
@@ -21,6 +22,7 @@ import { userSchemaResponse } from "@calcom/platform-types";
})
@UseGuards(ApiAuthGuard, PermissionsGuard)
@DocsTags("Me")
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
export class MeController {
constructor(
private readonly usersRepository: UsersRepository,
@@ -1,6 +1,7 @@
import { ProviderVerifyAccessTokenOutput } from "@/ee/provider/outputs/verify-access-token.output";
import { ProviderVerifyClientOutput } from "@/ee/provider/outputs/verify-client.output";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
@@ -16,7 +17,7 @@ import {
UnauthorizedException,
UseGuards,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiExcludeController, ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -24,6 +25,9 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
path: "/v2/provider",
version: API_VERSIONS_VALUES,
})
// note(Lauris): this controller is used by useOAuthClient and useOAuthFlow internal hooks and if customer
// wants to know if access token is expired then the flow is to make request and then receive message it expired.
@ApiExcludeController()
@DocsTags("Platform / Cal Provider")
export class CalProviderController {
constructor(private readonly oauthClientRepository: OAuthClientRepository) {}
@@ -53,6 +57,7 @@ export class CalProviderController {
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Verify an access token" })
@ApiHeader(ACCESS_TOKEN_HEADER)
async verifyAccessToken(
@Param("clientId") clientId: string,
@GetUser() user: UserWithProfile
@@ -1,5 +1,6 @@
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
import { VERSION_2024_06_14, VERSION_2024_06_11 } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -46,12 +47,7 @@ import {
default: VERSION_2024_06_11,
},
})
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
export class SchedulesController_2024_06_11 {
constructor(private readonly schedulesService: SchedulesService_2024_06_11) {}
+60
View File
@@ -0,0 +1,60 @@
import { ApiHeaderOptions } from "@nestjs/swagger";
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
export const OPTIONAL_X_CAL_CLIENT_ID_HEADER: ApiHeaderOptions = {
name: X_CAL_CLIENT_ID,
description: "For platform customers - OAuth client ID",
required: false,
};
export const OPTIONAL_X_CAL_SECRET_KEY_HEADER: ApiHeaderOptions = {
name: X_CAL_SECRET_KEY,
description: "For platform customers - OAuth client secret key",
required: false,
};
export const X_CAL_CLIENT_ID_HEADER: ApiHeaderOptions = {
name: X_CAL_CLIENT_ID,
description: "For platform customers - OAuth client ID",
required: false,
};
export const X_CAL_SECRET_KEY_HEADER: ApiHeaderOptions = {
name: X_CAL_SECRET_KEY,
description: "For platform customers - OAuth client secret key",
required: false,
};
export const OPTIONAL_API_KEY_HEADER: ApiHeaderOptions = {
name: "Authorization",
description:
"For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
required: false,
};
export const API_KEY_HEADER: ApiHeaderOptions = {
name: "Authorization",
description: "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
required: true,
};
export const API_KEY_OR_ACCESS_TOKEN_HEADER: ApiHeaderOptions = {
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
required: true,
};
export const OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER: ApiHeaderOptions = {
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
required: false,
};
export const ACCESS_TOKEN_HEADER: ApiHeaderOptions = {
name: "Authorization",
description: "value must be `Bearer <token>` where `<token>` is managed user access token",
required: true,
};
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { RefreshApiKeyInput } from "@/modules/api-keys/inputs/refresh-api-key.input";
import { RefreshApiKeyOutput } from "@/modules/api-keys/outputs/refresh-api-key.output";
import { ApiKeysService } from "@/modules/api-keys/services/api-keys.service";
@@ -23,12 +24,7 @@ export class ApiKeysController {
summary: "Refresh API Key",
description: `Generate a new API key and delete the current one. Provide API key to refresh as a Bearer token in the Authorization header (e.g. "Authorization: Bearer <apiKey>").`,
})
@ApiHeader({
name: "Authorization",
description: "Bearer <apiKey>",
required: true,
schema: { type: "string" },
})
@ApiHeader(API_KEY_HEADER)
@Post("/refresh")
@HttpCode(HttpStatus.OK)
async refresh(
@@ -9,7 +9,7 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
import { MembershipRole, getClientSecretFromPayment } from "@calcom/platform-libraries";
import { checkAdminOrOwner, getClientSecretFromPayment } from "@calcom/platform-libraries";
import type { TeamQuery } from "@calcom/platform-libraries";
import { enrichUserWithDelegationConferencingCredentialsWithoutOrgId } from "@calcom/platform-libraries/app-store";
import { getEnabledAppsFromCredentials, getAppFromSlug } from "@calcom/platform-libraries/app-store";
@@ -259,9 +259,7 @@ export class EventTypesAtomService {
name: team.name,
logoUrl: team.logoUrl,
credentialId: c.id,
isAdmin:
team.members[0].role === MembershipRole.ADMIN ||
team.members[0].role === MembershipRole.OWNER,
isAdmin: checkAdminOrOwner(team.members[0].role),
};
})
);
@@ -2,6 +2,7 @@ import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.g
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { RedisService } from "@/modules/redis/redis.service";
import { createMock } from "@golevelup/ts-jest";
import { ForbiddenException } from "@nestjs/common";
import { ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
@@ -47,11 +48,11 @@ describe("PlatformPlanGuard", () => {
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
});
it("should return false if the organization does not exist", async () => {
it("should throw ForbiddenException if the organization does not exist", async () => {
jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS");
jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue(null);
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
await expect(guard.canActivate(mockContext)).rejects.toThrow(ForbiddenException);
});
it("should return true if the organization is not platform", async () => {
@@ -66,7 +67,7 @@ describe("PlatformPlanGuard", () => {
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
});
it("should return false if the organization has no subscription", async () => {
it("should throw ForbiddenException if the organization has no subscription", async () => {
jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS");
jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue({
isPlatform: true,
@@ -78,10 +79,10 @@ describe("PlatformPlanGuard", () => {
},
});
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
await expect(guard.canActivate(mockContext)).rejects.toThrow(ForbiddenException);
});
it("should return false if the user has a lower plan than required", async () => {
it("should throw ForbiddenException if the user has a lower plan than required", async () => {
jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS");
jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue({
isPlatform: true,
@@ -93,7 +94,7 @@ describe("PlatformPlanGuard", () => {
},
});
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
await expect(guard.canActivate(mockContext)).rejects.toThrow(ForbiddenException);
});
it("should return true if the result is cached in Redis", async () => {
@@ -103,11 +104,11 @@ describe("PlatformPlanGuard", () => {
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
});
it("should return false if the result is cached in Redis", async () => {
it("should throw ForbiddenException if the result is cached as false in Redis", async () => {
jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS");
jest.spyOn(redisService.redis, "get").mockResolvedValue(JSON.stringify(false));
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
await expect(guard.canActivate(mockContext)).rejects.toThrow(ForbiddenException);
});
function createMockExecutionContext(context: Record<string, string | object>): ExecutionContext {
@@ -3,7 +3,7 @@ import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.st
import { PlatformPlanType } from "@/modules/billing/types";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Request } from "express";
@@ -55,7 +55,12 @@ export class PlatformPlanGuard implements CanActivate {
}
await this.redisService.redis.set(REDIS_CACHE_KEY, String(canAccess), "EX", 300);
return canAccess;
if (canAccess) {
return canAccess;
}
throw new ForbiddenException(
`Platform plan - you do not have required plan for this operation. Minimum plan is ${minimumPlan}.`
);
}
}
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import {
@@ -37,7 +38,7 @@ import {
Req,
HttpException,
} from "@nestjs/common";
import { ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToInstance } from "class-transformer";
import { Request } from "express";
@@ -70,6 +71,7 @@ export class ConferencingController {
@Post("/:app/connect")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Connect your conferencing application" })
@ApiParam({
name: "app",
@@ -98,6 +100,7 @@ export class ConferencingController {
@Get("/:app/oauth/auth-url")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Get OAuth conferencing app auth url" })
@ApiParam({
name: "app",
@@ -204,6 +207,7 @@ export class ConferencingController {
@Get("/")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "List your conferencing applications" })
async listInstalledConferencingApps(
@GetUser("id") userId: number
@@ -220,6 +224,7 @@ export class ConferencingController {
@Post("/:app/default")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Set your default conferencing application" })
@ApiParam({
name: "app",
@@ -238,6 +243,7 @@ export class ConferencingController {
@Get("/default")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Get your default conferencing application" })
async getDefault(@GetUser("id") userId: number): Promise<GetDefaultConferencingAppOutputResponseDto> {
const defaultconferencingApp = await this.conferencingService.getUserDefaultConferencingApp(userId);
@@ -247,6 +253,7 @@ export class ConferencingController {
@Delete("/:app/disconnect")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Disconnect your conferencing application" })
@ApiParam({
name: "app",
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
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";
@@ -9,7 +10,7 @@ import {
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 { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -19,11 +20,12 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
version: API_VERSIONS_VALUES,
})
@DocsTags("Destination Calendars")
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@UseGuards(ApiAuthGuard)
export class DestinationCalendarsController {
constructor(private readonly destinationCalendarsService: DestinationCalendarsService) {}
@Put("/")
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Update destination calendars" })
async updateDestinationCalendars(
@Body() input: DestinationCalendarsInputBodyDto,
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { GetWebhook } from "@/modules/webhooks/decorators/get-webhook-decorator";
import { IsUserEventTypeWebhookGuard } from "@/modules/webhooks/guards/is-user-event-type-webhook-guard";
@@ -25,7 +26,7 @@ import {
Patch,
ParseIntPipe,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { Webhook } from "@prisma/client";
import { plainToClass } from "class-transformer";
@@ -38,6 +39,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
})
@UseGuards(ApiAuthGuard, IsUserEventTypeWebhookGuard)
@DocsTags("Event Types / Webhooks")
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
export class EventTypeWebhooksController {
constructor(
private readonly webhooksService: WebhooksService,
@@ -31,11 +31,10 @@ import {
Query,
NotFoundException,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiOperation, ApiTags as DocsTags, ApiHeader } from "@nestjs/swagger";
import { User, MembershipRole } from "@prisma/client";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { Pagination } from "@calcom/platform-types";
import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
@Controller({
path: "/v2/oauth-clients/:clientId/users",
@@ -43,6 +42,11 @@ import { Pagination } from "@calcom/platform-types";
})
@UseGuards(ApiAuthGuard, OAuthClientGuard, OrganizationRolesGuard)
@DocsTags("Platform / Managed Users")
@ApiHeader({
name: X_CAL_SECRET_KEY,
description: "OAuth client secret key",
required: true,
})
export class OAuthClientUsersController {
private readonly logger = new Logger("UserController");
@@ -16,11 +16,11 @@ 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 as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { Webhook, MembershipRole } from "@prisma/client";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import { SkipTakePagination } from "@calcom/platform-types";
import { OAuthClientGuard } from "../../guards/oauth-client-guard";
@@ -31,6 +31,11 @@ import { OAuthClientGuard } from "../../guards/oauth-client-guard";
})
@UseGuards(NextAuthGuard, OrganizationRolesGuard, OAuthClientGuard)
@DocsTags("Platform / Webhooks")
@ApiHeader({
name: X_CAL_SECRET_KEY,
description: "OAuth client secret key",
required: true,
})
export class OAuthClientWebhooksController {
constructor(
private readonly webhooksService: WebhooksService,
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { GetOrgId } from "@/modules/auth/decorators/get-org-id/get-org-id.decorator";
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -9,7 +10,6 @@ import { CreateOAuthClientResponseDto } from "@/modules/oauth-clients/controller
import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto";
import { GetOAuthClientsResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientsResponse.dto";
import { OAuthClientGuard } from "@/modules/oauth-clients/guards/oauth-client-guard";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientsService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients.service";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { UsersRepository } from "@/modules/users/users.repository";
@@ -26,10 +26,15 @@ import {
HttpStatus,
Logger,
UseGuards,
NotFoundException,
BadRequestException,
} from "@nestjs/common";
import { ApiCreatedResponse as DocsCreatedResponse, ApiTags } from "@nestjs/swagger";
import {
ApiCreatedResponse as DocsCreatedResponse,
ApiTags,
ApiOperation,
ApiExcludeEndpoint,
ApiHeader,
} from "@nestjs/swagger";
import { User, MembershipRole } from "@prisma/client";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -41,11 +46,11 @@ import { CreateOAuthClientInput, UpdateOAuthClientInput, Pagination } from "@cal
})
@ApiTags("OAuth Clients")
@UseGuards(ApiAuthGuard, OrganizationRolesGuard)
@ApiHeader(API_KEY_HEADER)
export class OAuthClientsController {
private readonly logger = new Logger("OAuthClientController");
constructor(
private readonly oauthClientRepository: OAuthClientRepository,
private readonly oAuthClientsService: OAuthClientsService,
private readonly userRepository: UsersRepository,
private readonly teamsRepository: OrganizationsRepository
@@ -58,6 +63,7 @@ export class OAuthClientsController {
description: "Create an OAuth client",
type: CreateOAuthClientResponseDto,
})
@ApiOperation({ summary: "Create an OAuth client" })
async createOAuthClient(
@GetOrgId() organizationId: number,
@Body() body: CreateOAuthClientInput
@@ -68,7 +74,9 @@ export class OAuthClientsController {
const organization = await this.teamsRepository.findByIdIncludeBilling(organizationId);
if (!organization?.platformBilling || !organization?.platformBilling?.subscriptionId) {
throw new BadRequestException("Team is not subscribed, cannot create an OAuth Client.");
throw new BadRequestException(
"Team is not subscribed to platform plan, cannot create an OAuth Client."
);
}
const oAuthClientCredentials = await this.oAuthClientsService.createOAuthClient(organizationId, body);
@@ -81,6 +89,7 @@ export class OAuthClientsController {
@Get("/")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Get all OAuth clients" })
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
async getOAuthClients(@GetOrgId() organizationId: number): Promise<GetOAuthClientsResponseDto> {
const clients = await this.oAuthClientsService.getOAuthClients(organizationId);
@@ -91,6 +100,7 @@ export class OAuthClientsController {
@HttpCode(HttpStatus.OK)
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
@UseGuards(OAuthClientGuard)
@ApiOperation({ summary: "Get an OAuth client" })
async getOAuthClientById(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
const client = await this.oAuthClientsService.getOAuthClientById(clientId);
return { status: SUCCESS_STATUS, data: client };
@@ -100,6 +110,9 @@ export class OAuthClientsController {
@HttpCode(HttpStatus.OK)
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
@UseGuards(OAuthClientGuard)
// note(Lauris): Excluding this endpoint because we have oauth-clients-users.controller to fetch oAuth users and I am leaving this endpoint
// just in case someone is using it.
@ApiExcludeEndpoint()
async getOAuthClientManagedUsersById(
@Param("clientId") clientId: string,
@Query() queryParams: Pagination
@@ -118,6 +131,7 @@ export class OAuthClientsController {
@HttpCode(HttpStatus.OK)
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
@UseGuards(OAuthClientGuard)
@ApiOperation({ summary: "Update an OAuth client" })
async updateOAuthClient(
@Param("clientId") clientId: string,
@Body() body: UpdateOAuthClientInput
@@ -131,6 +145,7 @@ export class OAuthClientsController {
@HttpCode(HttpStatus.OK)
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
@UseGuards(OAuthClientGuard)
@ApiOperation({ summary: "Delete an OAuth client" })
async deleteOAuthClient(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
this.logger.log(`Deleting OAuth Client with ID: ${clientId}`);
const client = await this.oAuthClientsService.deleteOAuthClient(clientId);
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -28,7 +29,7 @@ import {
Query,
UseGuards,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { SkipTakePagination } from "@calcom/platform-types";
@@ -39,6 +40,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Attributes")
@ApiHeader(API_KEY_HEADER)
export class OrganizationsAttributesController {
constructor(private readonly organizationsAttributesService: OrganizationAttributesService) {}
// Gets all attributes for an organization
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -20,7 +21,7 @@ import { GetAllAttributeOptionOutput } from "@/modules/organizations/attributes/
import { UpdateAttributeOptionOutput } from "@/modules/organizations/attributes/options/outputs/update-option.output";
import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service";
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -30,6 +31,7 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Attributes / Options")
@ApiHeader(API_KEY_HEADER)
export class OrganizationsAttributesOptionsController {
constructor(private readonly organizationsAttributesOptionsService: OrganizationAttributeOptionService) {}
@@ -1,4 +1,9 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
OPTIONAL_API_KEY_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
@@ -23,7 +28,7 @@ import {
HttpStatus,
Patch,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { plainToClass } from "class-transformer";
@@ -37,6 +42,9 @@ import { CreateDelegationCredentialInput } from "./inputs/create-delegation-cred
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Delegation Credentials")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsDelegationCredentialController {
constructor(private readonly delegationCredentialService: OrganizationsDelegationCredentialService) {}
@@ -1,3 +1,5 @@
import { CalendarsModule } from "@/ee/calendars/calendars.module";
import { CALENDARS_QUEUE, CalendarsProcessor } from "@/ee/calendars/processors/calendars.processor";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OrganizationsDelegationCredentialController } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.controller";
import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository";
@@ -5,16 +7,31 @@ import { OrganizationsRepository } from "@/modules/organizations/index/organizat
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { BullModule } from "@nestjs/bull";
import { Module } from "@nestjs/common";
import { OrganizationsDelegationCredentialService } from "./services/organizations-delegation-credential.service";
@Module({
imports: [PrismaModule, StripeModule, RedisModule, MembershipsModule],
imports: [
PrismaModule,
StripeModule,
RedisModule,
CalendarsModule,
MembershipsModule,
BullModule.registerQueue({
name: CALENDARS_QUEUE,
limiter: {
max: 1,
duration: 1000,
},
}),
],
providers: [
OrganizationsDelegationCredentialService,
OrganizationsDelegationCredentialRepository,
OrganizationsRepository,
CalendarsProcessor,
],
controllers: [OrganizationsDelegationCredentialController],
})
@@ -28,4 +28,16 @@ export class OrganizationsDelegationCredentialRepository {
include: { workspacePlatform: true },
});
}
async findDelegatedUserProfiles(orgId: number, domain: string) {
return this.dbRead.prisma.profile.findMany({
select: {
userId: true,
},
where: {
organizationId: orgId,
user: { email: { endsWith: domain } },
},
});
}
}
@@ -1,3 +1,8 @@
import {
CALENDARS_QUEUE,
DEFAULT_CALENDARS_JOB,
DefaultCalendarsJobDataType,
} from "@/ee/calendars/processors/calendars.processor";
import { CreateDelegationCredentialInput } from "@/modules/organizations/delegation-credentials/inputs/create-delegation-credential.input";
import {
GoogleServiceAccountKeyInput,
@@ -5,9 +10,10 @@ import {
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
import { UpdateDelegationCredentialInput } from "@/modules/organizations/delegation-credentials/inputs/update-delegation-credential.input";
import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository";
import { DelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/delegation-credential.output";
import { Injectable, NotFoundException } from "@nestjs/common";
import { InjectQueue } from "@nestjs/bull";
import { Injectable, NotFoundException, Logger } from "@nestjs/common";
import { User } from "@prisma/client";
import { Queue } from "bull";
import { encryptServiceAccountKey } from "@calcom/platform-libraries";
import {
@@ -17,8 +23,11 @@ import {
@Injectable()
export class OrganizationsDelegationCredentialService {
private logger = new Logger("OrganizationsDelegationCredentialService");
constructor(
private readonly organizationsDelegationCredentialRepository: OrganizationsDelegationCredentialRepository
private readonly organizationsDelegationCredentialRepository: OrganizationsDelegationCredentialRepository,
@InjectQueue(CALENDARS_QUEUE) private readonly calendarsQueue: Queue
) {}
async createDelegationCredential(
orgId: number,
@@ -59,9 +68,44 @@ export class OrganizationsDelegationCredentialService {
await this.updateDelegationCredentialServiceAccountKey(delegationCredentialId, body.serviceAccountKey);
}
// once delegation credentials are enabled, slowly set all the destination calendars of delegated users
if (body.enabled === true && delegationCredential.enabled === false) {
await this.ensureDefaultCalendars(orgId, delegationCredential.domain);
}
return { ...delegationCredential, enabled: body?.enabled ?? delegationCredential?.enabled };
}
async ensureDefaultCalendars(orgId: number, domain: string) {
try {
const delegatedUserProfiles =
await this.organizationsDelegationCredentialRepository.findDelegatedUserProfiles(orgId, domain);
delegatedUserProfiles.forEach(async (profile) => {
if (profile.userId) {
const job = await this.calendarsQueue.getJob(`${DEFAULT_CALENDARS_JOB}_${profile.userId}`);
if (job) {
await job.remove();
this.logger.log(`Removed default calendar job for user with id: ${profile.userId}`);
}
this.logger.log(`Adding default calendar job for user with id: ${profile.userId}`);
await this.calendarsQueue.add(
DEFAULT_CALENDARS_JOB,
{
userId: profile.userId,
} satisfies DefaultCalendarsJobDataType,
{ jobId: `${DEFAULT_CALENDARS_JOB}_${profile.userId}`, removeOnComplete: true }
);
}
});
} catch (err) {
this.logger.error(
err,
`Could not ensure default calendars for delegated users in org with id:${orgId}`
);
}
}
async updateDelegationCredentialEnabled(
orgId: number,
delegationCredentialId: string,
@@ -0,0 +1,481 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
import {
GetBookingsDataEntry,
GetBookingsOutput_2024_04_15,
} from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { Locales } from "@/lib/enums/locales";
import {
CreateManagedUserData,
CreateManagedUserOutput,
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { CreateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/create-organization-team.input";
import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input";
import { OrgTeamMembershipOutputDto } from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
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 } from "@prisma/client";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import {
CAL_API_VERSION_HEADER,
SUCCESS_STATUS,
VERSION_2024_06_14,
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
} from "@calcom/platform-constants";
import {
ApiSuccessResponse,
CreateEventTypeInput_2024_06_14,
CreateTeamEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
Host,
OrgTeamOutputDto,
TeamEventTypeOutput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
const CLIENT_REDIRECT_URI = "http://localhost:4321";
describe("Assign all team members", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let hostsRepositoryFixture: HostsRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
const platformAdminEmail = `assign-all-team-members-admin-${randomString()}@api.com`;
let platformAdmin: User;
let managedTeam: OrgTeamOutputDto;
const managedUsersTimeZone = "Europe/Rome";
const firstManagedUserEmail = `managed-user-bookings-2024-04-15-first-user@api.com`;
const secondManagedUserEmail = `managed-user-bookings-2024-04-15-second-user@api.com`;
let firstManagedUser: CreateManagedUserData;
let secondManagedUser: CreateManagedUserData;
let roundRobinEventType: TeamEventTypeOutput_2024_06_14;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule],
}).compile();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
organization = await teamRepositoryFixture.create({
name: `oauth-client-users-organization-${randomString()}`,
isPlatform: true,
isOrganization: true,
platformBilling: {
create: {
customerId: "cus_999",
plan: "ESSENTIALS",
subscriptionId: "sub_999",
},
},
});
oAuthClient = await createOAuthClient(organization.id);
await profilesRepositoryFixture.create({
uid: randomString(),
username: platformAdminEmail,
organization: { connect: { id: organization.id } },
user: {
connect: { id: platformAdmin.id },
},
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: platformAdmin.id } },
team: { connect: { id: organization.id } },
accepted: true,
});
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: [CLIENT_REDIRECT_URI],
permissions: 1023,
areDefaultEventTypesEnabled: false,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
describe("setup managed users", () => {
it("should create first managed user", async () => {
const requestBody: CreateManagedUserInput = {
email: firstManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Alice Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
firstManagedUser = responseBody.data;
});
it("should create second managed user", async () => {
const requestBody: CreateManagedUserInput = {
email: secondManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Bob Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
secondManagedUser = responseBody.data;
});
});
describe("should setup managed team", () => {
it("should create managed team", async () => {
const body: CreateOrgTeamDto = {
name: `team-${randomString()}`,
};
return request(app.getHttpServer())
.post(`/v2/organizations/${organization.id}/teams`)
.send(body)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.set(X_CAL_CLIENT_ID, oAuthClient.id)
.expect(201)
.then((response) => {
const responseBody: ApiSuccessResponse<OrgTeamOutputDto> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
managedTeam = responseBody.data;
});
});
});
describe("should setup memberships", () => {
it("should create first user's membership of the org's team", async () => {
const body: CreateOrgTeamMembershipDto = {
userId: firstManagedUser.user.id,
accepted: true,
role: "MEMBER",
};
return request(app.getHttpServer())
.post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/memberships`)
.send(body)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.set(X_CAL_CLIENT_ID, oAuthClient.id)
.expect(201)
.then((response) => {
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
});
});
it("should create second user's membership of the org's team", async () => {
const body: CreateOrgTeamMembershipDto = {
userId: secondManagedUser.user.id,
accepted: true,
role: "MEMBER",
};
return request(app.getHttpServer())
.post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/memberships`)
.send(body)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.set(X_CAL_CLIENT_ID, oAuthClient.id)
.expect(201)
.then((response) => {
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
});
});
});
describe("should setup event types using assignAllTeamMembers true", () => {
it("should not be able to setup team event type if no hosts nor assignAllTeamMembers provided", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: "Coding consultation round robin",
slug: `organizations-event-types-round-robin-${randomString()}`,
lengthInMinutes: 60,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
schedulingType: "collective",
};
const response = await request(app.getHttpServer())
.post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types`)
.send(body)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.set(X_CAL_CLIENT_ID, oAuthClient.id)
.expect(400);
});
it("should setup collective event type assignAllTeamMembers true", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: "Coding consultation collective",
slug: `assign-all-team-members-collective-${randomString()}`,
lengthInMinutes: 60,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
schedulingType: "collective",
assignAllTeamMembers: true,
};
return request(app.getHttpServer())
.post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types`)
.send(body)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.set(X_CAL_CLIENT_ID, oAuthClient.id)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data.title).toEqual(body.title);
expect(data.hosts.length).toEqual(2);
expect(data.schedulingType).toEqual("collective");
evaluateHost({ userId: firstManagedUser.user.id }, data.hosts[0]);
evaluateHost({ userId: secondManagedUser.user.id }, data.hosts[1]);
const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id);
expect(eventTypeHosts.length).toEqual(2);
const firstHost = eventTypeHosts.find((host) => host.userId === firstManagedUser.user.id);
const secondHost = eventTypeHosts.find((host) => host.userId === secondManagedUser.user.id);
expect(firstHost).toBeDefined();
expect(secondHost).toBeDefined();
});
});
it("should setup round robin event type assignAllTeamMembers true", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: "Coding consultation round robin",
slug: `assign-all-team-members-round-robin-${randomString()}`,
lengthInMinutes: 60,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
schedulingType: "roundRobin",
assignAllTeamMembers: true,
};
return request(app.getHttpServer())
.post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types`)
.send(body)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.set(X_CAL_CLIENT_ID, oAuthClient.id)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data.title).toEqual(body.title);
expect(data.hosts.length).toEqual(2);
expect(data.schedulingType).toEqual("roundRobin");
evaluateHost(
{ userId: firstManagedUser.user.id, mandatory: false, priority: "medium" },
data.hosts[0]
);
evaluateHost(
{ userId: secondManagedUser.user.id, mandatory: false, priority: "medium" },
data.hosts[1]
);
const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id);
expect(eventTypeHosts.length).toEqual(2);
const firstHost = eventTypeHosts.find((host) => host.userId === firstManagedUser.user.id);
const secondHost = eventTypeHosts.find((host) => host.userId === secondManagedUser.user.id);
expect(firstHost).toBeDefined();
expect(secondHost).toBeDefined();
roundRobinEventType = data;
});
});
it("should update round robin event type", async () => {
const body: UpdateTeamEventTypeInput_2024_06_14 = {
title: "Coding consultation round robin updated",
};
return request(app.getHttpServer())
.patch(
`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types/${roundRobinEventType.id}`
)
.send(body)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.set(X_CAL_CLIENT_ID, oAuthClient.id)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data.title).toEqual(body.title);
expect(data.hosts.length).toEqual(2);
expect(data.schedulingType).toEqual("roundRobin");
evaluateHost(
{ userId: firstManagedUser.user.id, mandatory: false, priority: "medium" },
data.hosts[0]
);
evaluateHost(
{ userId: secondManagedUser.user.id, mandatory: false, priority: "medium" },
data.hosts[1]
);
const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id);
expect(eventTypeHosts.length).toEqual(2);
const firstHost = eventTypeHosts.find((host) => host.userId === firstManagedUser.user.id);
const secondHost = eventTypeHosts.find((host) => host.userId === secondManagedUser.user.id);
expect(firstHost).toBeDefined();
expect(secondHost).toBeDefined();
});
});
it("should setup managed event type assignAllTeamMembers true", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: "Coding consultation managed",
slug: `assign-all-team-members-managed-${randomString()}`,
lengthInMinutes: 60,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
schedulingType: "managed",
assignAllTeamMembers: true,
};
return request(app.getHttpServer())
.post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types`)
.send(body)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.set(X_CAL_CLIENT_ID, oAuthClient.id)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data.length).toEqual(3);
const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(
firstManagedUser.user.id
);
const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(
secondManagedUser.user.id
);
const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(managedTeam.id);
const managedTeamEventTypes = teamEventTypes.filter(
(eventType) => eventType.schedulingType === "MANAGED"
);
expect(teammate1EventTypes.length).toEqual(1);
expect(teammate2EventTypes.length).toEqual(1);
expect(managedTeamEventTypes.length).toEqual(1);
expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(true);
const responseTeamEvent = responseBody.data.find(
(eventType) => eventType.schedulingType === "managed"
);
expect(responseTeamEvent).toBeDefined();
expect(responseTeamEvent?.teamId).toEqual(managedTeam.id);
expect(responseTeamEvent?.assignAllTeamMembers).toEqual(true);
const responseTeammate1Event = responseBody.data.find(
(eventType) => eventType.ownerId === firstManagedUser.user.id
);
expect(responseTeammate1Event).toBeDefined();
expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id);
const responseTeammate2Event = responseBody.data.find(
(eventType) => eventType.ownerId === secondManagedUser.user.id
);
expect(responseTeammate2Event).toBeDefined();
expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id);
});
});
});
function evaluateHost(expected: Host, received: Host | undefined) {
expect(expected.userId).toEqual(received?.userId);
expect(expected.mandatory).toEqual(received?.mandatory);
expect(expected.priority).toEqual(received?.priority);
}
afterAll(async () => {
await userRepositoryFixture.delete(firstManagedUser.user.id);
await userRepositoryFixture.delete(secondManagedUser.user.id);
await userRepositoryFixture.delete(platformAdmin.id);
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await app.close();
});
});
@@ -1,6 +1,11 @@
import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input";
import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_API_KEY_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
@@ -35,7 +40,7 @@ import {
NotFoundException,
Query,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
import { handleCreatePhoneCall } from "@calcom/platform-libraries";
@@ -56,7 +61,10 @@ export type EventTypeHandlerResponse = {
path: "/v2/organizations/:orgId",
version: API_VERSIONS_VALUES,
})
@DocsTags("Orgs / Event Types")
@DocsTags("Orgs / Teams / Event Types")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsEventTypesController {
constructor(
private readonly organizationsEventTypesService: OrganizationsEventTypesService,
@@ -324,9 +324,9 @@ describe("Organizations Event Types Endpoints", () => {
const data = responseBody.data;
expect(data.title).toEqual(body.title);
expect(data.hosts.length).toEqual(2);
expect(data.schedulingType).toEqual("COLLECTIVE");
evaluateHost(body.hosts[0], data.hosts[0]);
evaluateHost(body.hosts[1], data.hosts[1]);
expect(data.schedulingType).toEqual("collective");
evaluateHost(body.hosts?.[0] || { userId: -1 }, data.hosts[0]);
evaluateHost(body.hosts?.[1] || { userId: -1 }, data.hosts[1]);
expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount);
expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
@@ -457,8 +457,8 @@ describe("Organizations Event Types Endpoints", () => {
const data = responseBody.data;
expect(data.length).toEqual(2);
const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "COLLECTIVE");
const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "MANAGED");
const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "collective");
const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "managed");
expect(eventTypeCollective?.title).toEqual(collectiveEventType.title);
expect(eventTypeCollective?.hosts.length).toEqual(2);
@@ -564,7 +564,7 @@ describe("Organizations Event Types Endpoints", () => {
).toEqual(newTitle);
const responseTeamEvent = responseBody.data.find(
(eventType) => eventType.schedulingType === "MANAGED"
(eventType) => eventType.schedulingType === "managed"
);
expect(responseTeamEvent).toBeDefined();
expect(responseTeamEvent?.title).toEqual(newTitle);
@@ -823,7 +823,7 @@ describe("Organizations Event Types Endpoints", () => {
expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(true);
const responseTeamEvent = responseBody.data.find(
(eventType) => eventType.schedulingType === "MANAGED"
(eventType) => eventType.schedulingType === "managed"
);
expect(responseTeamEvent).toBeDefined();
expect(responseTeamEvent?.teamId).toEqual(team.id);
@@ -954,6 +954,117 @@ describe("Organizations Event Types Endpoints", () => {
});
});
it("should create a round robin team event-type", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
successRedirectUrl: "https://masterchief.com/argentina/flan/video/1234",
title: "Coding consultation round robin",
slug: `organizations-event-types-round-robin-${randomString()}`,
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
bookingFields: [
{
type: "select",
label: "select which language is your codebase in",
slug: "select-language",
required: true,
placeholder: "select language",
options: ["javascript", "python", "cobol"],
},
],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
schedulingType: "roundRobin",
hosts: [
{
userId: teammate1.id,
mandatory: true,
priority: "high",
},
{
userId: teammate2.id,
mandatory: false,
priority: "medium",
},
],
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,
},
bookerLayouts: {
enabledLayouts: [
BookerLayoutsInputEnum_2024_06_14.column,
BookerLayoutsInputEnum_2024_06_14.month,
BookerLayoutsInputEnum_2024_06_14.week,
],
defaultLayout: BookerLayoutsInputEnum_2024_06_14.month,
},
confirmationPolicy: {
type: ConfirmationPolicyEnum.TIME,
noticeThreshold: {
count: 60,
unit: NoticeThresholdUnitEnum.MINUTES,
},
blockUnconfirmedBookingsInBooker: true,
},
requiresBookerEmailVerification: true,
hideCalendarNotes: true,
hideCalendarEventDetails: true,
lockTimeZoneToggleOnBookingPage: true,
color: {
darkThemeHex: "#292929",
lightThemeHex: "#fafafa",
},
};
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`)
.send(body)
.expect(201)
.then((response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data.title).toEqual(body.title);
expect(data.hosts.length).toEqual(2);
expect(data.schedulingType).toEqual("roundRobin");
evaluateHost(body.hosts?.[0] || { userId: -1 }, data.hosts[0]);
evaluateHost(body.hosts?.[1] || { userId: -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);
expect(data.bookerLayouts).toEqual(body.bookerLayouts);
expect(data.confirmationPolicy).toEqual(body.confirmationPolicy);
expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification);
expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes);
expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
expect(data.color).toEqual(body.color);
expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234");
collectiveEventType = responseBody.data;
});
});
function evaluateHost(expected: Host, received: Host | undefined) {
expect(expected.userId).toEqual(received?.userId);
expect(expected.mandatory).toEqual(received?.mandatory);
@@ -33,6 +33,7 @@ export class InputOrganizationsEventTypesService {
inputEventType: CreateTeamEventTypeInput_2024_06_14
) {
await this.validateHosts(teamId, inputEventType.hosts);
await this.validateTeamEventTypeSlug(teamId, inputEventType.slug);
const transformedBody = await this.transformInputCreateTeamEventType(teamId, inputEventType);
@@ -62,6 +63,9 @@ export class InputOrganizationsEventTypesService {
inputEventType: UpdateTeamEventTypeInput_2024_06_14
) {
await this.validateHosts(teamId, inputEventType.hosts);
if (inputEventType.slug) {
await this.validateTeamEventTypeSlug(teamId, inputEventType.slug);
}
const transformedBody = await this.transformInputUpdateTeamEventType(eventTypeId, teamId, inputEventType);
@@ -85,10 +89,28 @@ export class InputOrganizationsEventTypesService {
return transformedBody;
}
async validateTeamEventTypeSlug(teamId: number, slug: string) {
const teamEventWithSlugExists = await this.teamsEventTypesRepository.getEventTypeByTeamIdAndSlug(
teamId,
slug
);
if (teamEventWithSlugExists) {
throw new BadRequestException("Team event type with this slug already exists");
}
}
async transformInputCreateTeamEventType(
teamId: number,
inputEventType: CreateTeamEventTypeInput_2024_06_14
) {
const hasHosts = !!inputEventType.hosts && !!inputEventType.hosts.length;
const hasAssignAllTeamMembers = inputEventType.assignAllTeamMembers === true;
if (!hasHosts && !hasAssignAllTeamMembers) {
throw new BadRequestException("Either hosts must be provided or assignAllTeamMembers must be true");
}
const { hosts, assignAllTeamMembers, locations, ...rest } = inputEventType;
const eventType = this.inputEventTypesService.transformInputCreateEventType(rest);
@@ -185,7 +207,7 @@ export class InputOrganizationsEventTypesService {
eventType: { children: { userId: number | null }[] } | null
) {
if (inputEventType.assignAllTeamMembers) {
return await this.teamsRepository.getTeamMembersIds(teamId);
return await this.getTeamUsersIds(teamId);
}
// note(Lauris): when API user updates managed event type users
@@ -197,6 +219,17 @@ export class InputOrganizationsEventTypesService {
return eventType?.children.map((child) => child.userId).filter((id) => !!id) as number[];
}
async getTeamUsersIds(teamId: number) {
const team = await this.teamsRepository.getById(teamId);
const isPlatformTeam = !!team?.createdByOAuthClientId;
if (isPlatformTeam) {
// note(Lauris): platform team creators have role "OWNER" but we don't want to assign them to team members marked as "assignAllTeamMembers: true"
// because they are not a managed user.
return await this.teamsRepository.getTeamManagedUsersIds(teamId);
}
return await this.teamsRepository.getTeamUsersIds(teamId);
}
transformInputTeamLocations(inputLocations: CreateTeamEventTypeInput_2024_06_14["locations"]) {
return transformTeamLocationsApiToInternal(inputLocations);
}
@@ -217,7 +250,7 @@ export class InputOrganizationsEventTypesService {
}
async getAllTeamMembers(teamId: number, schedulingType: SchedulingType | null) {
const membersIds = await this.teamsRepository.getTeamMembersIds(teamId);
const membersIds = await this.getTeamUsersIds(teamId);
const isFixed = schedulingType === "COLLECTIVE" ? true : false;
return membersIds.map((id) => ({
@@ -249,7 +282,7 @@ export class InputOrganizationsEventTypesService {
async validateHosts(teamId: number, hosts: CreateTeamEventTypeInput_2024_06_14["hosts"] | undefined) {
if (hosts && hosts.length) {
const membersIds = await this.teamsRepository.getTeamMembersIds(teamId);
const membersIds = await this.getTeamUsersIds(teamId);
const invalidHosts = hosts.filter((host) => !membersIds.includes(host.userId));
if (invalidHosts.length) {
throw new NotFoundException(
@@ -100,7 +100,9 @@ export class OutputOrganizationsEventTypesService {
teamId,
ownerId: userId,
parentEventTypeId: parentId,
schedulingType: databaseEventType.schedulingType,
schedulingType: databaseEventType.schedulingType
? this.getResponseSchedulingType(databaseEventType.schedulingType)
: databaseEventType.schedulingType,
assignAllTeamMembers: teamId ? assignAllTeamMembers : undefined,
team: {
id: teamId,
@@ -116,6 +118,19 @@ export class OutputOrganizationsEventTypesService {
};
}
getResponseSchedulingType(schedulingType: SchedulingType) {
if (schedulingType === SchedulingType.COLLECTIVE) {
return "collective";
}
if (schedulingType === SchedulingType.ROUND_ROBIN) {
return "roundRobin";
}
if (schedulingType === SchedulingType.MANAGED) {
return "managed";
}
return schedulingType;
}
async getManagedEventTypeHosts(eventTypeId: number) {
const children = await this.teamsEventTypesRepository.getEventTypeChildren(eventTypeId);
const transformedHosts: TeamEventTypeResponseHost[] = [];
@@ -144,7 +159,7 @@ export class OutputOrganizationsEventTypesService {
transformedHosts.push({
userId: databaseHost.userId,
name: databaseUser?.name || "",
mandatory: databaseHost.isFixed,
mandatory: !!databaseHost.isFixed,
priority: getPriorityLabel(databaseHost.priority || 2),
avatarUrl: databaseUser?.avatarUrl,
});
@@ -13,7 +13,10 @@ export class CreateOrgMembershipDto {
readonly accepted?: boolean = false;
@IsEnum(MembershipRole)
@ApiProperty({ enum: ["MEMBER", "OWNER", "ADMIN"] })
@ApiProperty({
enum: ["MEMBER", "OWNER", "ADMIN"],
description: "If you are platform customer then managed users should only have MEMBER role.",
})
readonly role: MembershipRole = MembershipRole.MEMBER;
@IsOptional()
@@ -1,4 +1,9 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_API_KEY_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -30,7 +35,7 @@ import {
HttpCode,
HttpStatus,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -42,6 +47,9 @@ import { SkipTakePagination } from "@calcom/platform-types";
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Memberships")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsMembershipsController {
constructor(private organizationsMembershipService: OrganizationsMembershipService) {}
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { X_CAL_CLIENT_ID_HEADER, X_CAL_SECRET_KEY_HEADER } from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
@@ -28,7 +29,7 @@ import {
HttpStatus,
Delete,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -40,6 +41,8 @@ const SCALE = "SCALE";
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Orgs")
@ApiHeader(X_CAL_CLIENT_ID_HEADER)
@ApiHeader(X_CAL_SECRET_KEY_HEADER)
export class OrganizationsOrganizationsController {
constructor(private readonly managedOrganizationsService: ManagedOrganizationsService) {}
@@ -1,5 +1,10 @@
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_API_KEY_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -23,7 +28,7 @@ import {
HttpStatus,
Query,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import {
@@ -42,7 +47,9 @@ import { SkipTakePagination } from "@calcom/platform-types";
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Schedules")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsSchedulesController {
constructor(
private schedulesService: SchedulesService_2024_06_11,
@@ -52,6 +59,7 @@ export class OrganizationsSchedulesController {
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@Get("/schedules")
@DocsTags("Orgs / Schedules")
@ApiOperation({ summary: "Get all schedules" })
async getOrganizationSchedules(
@Param("orgId", ParseIntPipe) orgId: number,
@@ -1,6 +1,11 @@
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
OPTIONAL_API_KEY_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
@@ -13,7 +18,7 @@ import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
import { GetOrganizationsTeamsBookingsInput_2024_08_13 } from "@/modules/organizations/teams/bookings/inputs/get-organizations-teams-bookings.input";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query, HttpStatus, HttpCode } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
@@ -24,6 +29,9 @@ import { GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Teams / Bookings")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsTeamsBookingsController {
constructor(private readonly bookingsService: BookingsService_2024_08_13) {}
@@ -1,4 +1,9 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_API_KEY_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetTeam } from "@/modules/auth/decorators/get-team/get-team.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
@@ -30,10 +35,11 @@ import {
Patch,
Post,
Body,
Headers,
Req,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { Request } from "express";
import { SUCCESS_STATUS, X_CAL_CLIENT_ID } from "@calcom/platform-constants";
import { OrgTeamOutputDto } from "@calcom/platform-types";
@@ -46,11 +52,13 @@ import { Team } from "@calcom/prisma/client";
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Teams")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsTeamsController {
constructor(private organizationsTeamsService: OrganizationsTeamsService) {}
@Get()
@DocsTags("Teams")
@ApiOperation({ summary: "Get all teams" })
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@@ -148,8 +156,9 @@ export class OrganizationsTeamsController {
@Param("orgId", ParseIntPipe) orgId: number,
@Body() body: CreateOrgTeamDto,
@GetUser() user: UserWithProfile,
@Headers(X_CAL_CLIENT_ID) oAuthClientId?: string
@Req() req: Request
): Promise<OrgTeamOutputResponseDto> {
const oAuthClientId = req.headers[X_CAL_CLIENT_ID] as string | undefined;
const team = oAuthClientId
? await this.organizationsTeamsService.createPlatformOrgTeam(orgId, oAuthClientId, body, user)
: await this.organizationsTeamsService.createOrgTeam(orgId, body, user);
@@ -1,4 +1,9 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_API_KEY_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -33,7 +38,7 @@ import {
UnprocessableEntityException,
Logger,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -46,6 +51,9 @@ import { SkipTakePagination } from "@calcom/platform-types";
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Teams / Memberships")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsTeamsMembershipsController {
private logger = new Logger("OrganizationsTeamsMembershipsController");
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -8,11 +9,9 @@ import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { IsRoutingFormInTeam } from "@/modules/auth/guards/routing-forms/is-routing-form-in-team.guard";
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
import { Controller, Get, Param, UseGuards } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { RoutingFormResponseOutput } from "@calcom/platform-types";
import { RoutingFormsResponsesService } from "../../../../routing-forms-responses/services/routing-forms-responses.service";
import { GetRoutingFormResponsesOutput } from "../outputs/get-routing-form-responses.output";
@@ -30,6 +29,7 @@ import { GetRoutingFormResponsesOutput } from "../outputs/get-routing-form-respo
PlatformPlanGuard,
IsAdminAPIEnabledGuard
)
@ApiHeader(API_KEY_HEADER)
export class OrganizationsTeamsRoutingFormsResponsesController {
constructor(private readonly routingFormsResponsesService: RoutingFormsResponsesService) {}
@@ -1,5 +1,10 @@
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_API_KEY_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -10,7 +15,7 @@ import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
import { IsUserInOrgTeam } from "@/modules/auth/guards/users/is-user-in-org-team.guard";
import { Controller, UseGuards, Get, Param, ParseIntPipe } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
@@ -20,7 +25,9 @@ import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, IsTeamInOrg, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Teams / Schedules")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsTeamsSchedulesController {
constructor(private schedulesService: SchedulesService_2024_06_11) {}
@@ -29,7 +36,9 @@ export class OrganizationsTeamsSchedulesController {
@UseGuards(IsUserInOrgTeam)
@Get("/users/:userId/schedules")
@DocsTags("Orgs / Teams / Users / Schedules")
@ApiOperation({ summary: "Get schedules of a team member" })
@ApiOperation({
summary: "Get schedules of a team member",
})
async getUserSchedules(
@Param("userId", ParseIntPipe) userId: number
): Promise<GetSchedulesOutput_2024_06_11> {
@@ -1,4 +1,9 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_API_KEY_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetOrg } from "@/modules/auth/decorators/get-org/get-org.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
@@ -33,7 +38,7 @@ import {
Query,
} from "@nestjs/common";
import { ClassSerializerInterceptor } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToInstance } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -47,6 +52,9 @@ import { Team } from "@calcom/prisma/client";
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@UseGuards(IsOrgGuard)
@DocsTags("Orgs / Users")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsUsersController {
constructor(private readonly organizationsUsersService: OrganizationsUsersService) {}
@@ -1,4 +1,9 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_API_KEY_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -35,7 +40,7 @@ import {
Query,
} from "@nestjs/common";
import { ClassSerializerInterceptor } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToInstance } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -48,6 +53,9 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@UseGuards(IsOrgGuard)
@DocsTags("Orgs / Users / OOO")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsUsersOOOController {
constructor(
private readonly userOOOService: UserOOOService,
@@ -1,4 +1,9 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_API_KEY_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -32,7 +37,7 @@ import {
HttpCode,
HttpStatus,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -44,6 +49,9 @@ import { SkipTakePagination } from "@calcom/platform-types";
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Webhooks")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsWebhooksController {
constructor(
private organizationsWebhooksService: OrganizationsWebhooksService,
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import {
@@ -12,7 +13,7 @@ import {
import { SelectedCalendarsService } from "@/modules/selected-calendars/services/selected-calendars.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Body, Controller, Post, UseGuards, Delete, Query } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -22,11 +23,12 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
version: API_VERSIONS_VALUES,
})
@DocsTags("Selected Calendars")
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
export class SelectedCalendarsController {
constructor(private readonly selectedCalendarsService: SelectedCalendarsService) {}
@Post("/")
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Add a selected calendar" })
async addSelectedCalendar(
@Body() input: SelectedCalendarsInputDto,
@@ -41,7 +43,6 @@ export class SelectedCalendarsController {
}
@Delete("/")
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Delete a selected calendar" })
async deleteSelectedCalendar(
@Query() queryParams: SelectedCalendarsQueryParamsInputDto,
@@ -1,4 +1,5 @@
import { VERSION_2024_09_04 } from "@/lib/api-versions";
import { OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER, OPTIONAL_X_CAL_CLIENT_ID_HEADER } from "@/lib/docs/headers";
import { GetOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
import { GetReservedSlotOutput_2024_09_04 } from "@/modules/slots/slots-2024-09-04/outputs/get-reserved-slot.output";
@@ -13,12 +14,10 @@ import {
Delete,
Post,
Param,
Res,
HttpCode,
HttpStatus,
Patch,
UseGuards,
BadRequestException,
} from "@nestjs/common";
import {
ApiOperation,
@@ -223,8 +222,12 @@ export class SlotsController_2024_09_04 {
@UseGuards(OptionalApiAuthGuard)
@ApiOperation({
summary: "Reserve a slot",
description: "Make a slot not available for others to book for a certain period of time.",
description: `Make a slot not available for others to book for a certain period of time. If you authenticate using oAuth credentials, api key or access token
then you can also specify custom duration for how long the slot should be reserved for (defaults to 5 minutes).`,
})
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
async reserveSlot(
@Body() body: ReserveSlotInput_2024_09_04,
@GetOptionalUser() user: User
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import {
@@ -23,7 +24,7 @@ import {
Headers,
Param,
} from "@nestjs/common";
import { ApiTags as DocsTags, ApiOperation } from "@nestjs/swagger";
import { ApiTags as DocsTags, ApiOperation, ApiHeader } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { Request } from "express";
import { stringify } from "querystring";
@@ -42,6 +43,7 @@ export class StripeController {
@UseGuards(ApiAuthGuard)
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Get stripe connect URL" })
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
async redirect(
@Req() req: Request,
@Headers("Authorization") authorization: string,
@@ -100,6 +102,7 @@ export class StripeController {
@Get("/check")
@UseGuards(ApiAuthGuard)
@HttpCode(HttpStatus.OK)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Check stripe connection" })
async check(@GetUser() user: UserWithProfile): Promise<StripCredentialsCheckOutputResponseDto> {
return await this.stripeService.checkIfIndividualStripeAccountConnected(user.id);
@@ -108,6 +111,7 @@ export class StripeController {
@Get("/check/:teamId")
@UseGuards(ApiAuthGuard)
@HttpCode(HttpStatus.OK)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Check team stripe connection" })
async checkTeamStripeConnection(
@Param("teamId") teamId: string
@@ -303,9 +303,9 @@ describe("Organizations Event Types Endpoints", () => {
const data = responseBody.data;
expect(data.title).toEqual(body.title);
expect(data.hosts.length).toEqual(2);
expect(data.schedulingType).toEqual("COLLECTIVE");
evaluateHost(body.hosts[0], data.hosts[0]);
evaluateHost(body.hosts[1], data.hosts[1]);
expect(data.schedulingType).toEqual("collective");
evaluateHost(body.hosts?.[0] || { userId: -1 }, data.hosts[0]);
evaluateHost(body.hosts?.[1] || { userId: -1 }, data.hosts[1]);
expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount);
expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
@@ -423,8 +423,8 @@ describe("Organizations Event Types Endpoints", () => {
const data = responseBody.data;
expect(data.length).toEqual(2);
const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "COLLECTIVE");
const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "MANAGED");
const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "collective");
const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "managed");
expect(eventTypeCollective?.title).toEqual(collectiveEventType.title);
expect(eventTypeCollective?.hosts.length).toEqual(2);
@@ -516,7 +516,7 @@ describe("Organizations Event Types Endpoints", () => {
).toEqual(newTitle);
const responseTeamEvent = responseBody.data.find(
(eventType) => eventType.schedulingType === "MANAGED"
(eventType) => eventType.schedulingType === "managed"
);
expect(responseTeamEvent).toBeDefined();
expect(responseTeamEvent?.title).toEqual(newTitle);
@@ -562,7 +562,7 @@ describe("Organizations Event Types Endpoints", () => {
expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(true);
const responseTeamEvent = responseBody.data.find(
(eventType) => eventType.schedulingType === "MANAGED"
(eventType) => eventType.schedulingType === "managed"
);
expect(responseTeamEvent).toBeDefined();
expect(responseTeamEvent?.teamId).toEqual(team.id);
@@ -1,6 +1,7 @@
import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input";
import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
@@ -31,7 +32,7 @@ import {
NotFoundException,
Query,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
import { handleCreatePhoneCall } from "@calcom/platform-libraries";
@@ -63,6 +64,7 @@ export class TeamsEventTypesController {
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(ApiAuthGuard, RolesGuard)
@ApiHeader(API_KEY_HEADER)
@Post("/")
@ApiOperation({ summary: "Create an event type" })
async createTeamEventType(
@@ -86,6 +88,7 @@ export class TeamsEventTypesController {
@Roles("TEAM_ADMIN")
@UseGuards(ApiAuthGuard, RolesGuard)
@ApiHeader(API_KEY_HEADER)
@Get("/:eventTypeId")
@ApiOperation({ summary: "Get an event type" })
async getTeamEventType(
@@ -109,6 +112,7 @@ export class TeamsEventTypesController {
@Roles("TEAM_ADMIN")
@Post("/:eventTypeId/create-phone-call")
@UseGuards(ApiAuthGuard, RolesGuard)
@ApiHeader(API_KEY_HEADER)
@ApiOperation({ summary: "Create a phone call" })
async createPhoneCall(
@Param("eventTypeId") eventTypeId: number,
@@ -162,6 +166,7 @@ export class TeamsEventTypesController {
@Roles("TEAM_ADMIN")
@UseGuards(ApiAuthGuard, RolesGuard)
@ApiHeader(API_KEY_HEADER)
@Patch("/:eventTypeId")
@ApiOperation({ summary: "Update a team event type" })
async updateTeamEventType(
@@ -193,6 +198,7 @@ export class TeamsEventTypesController {
@Roles("TEAM_ADMIN")
@UseGuards(ApiAuthGuard, RolesGuard)
@ApiHeader(API_KEY_HEADER)
@Delete("/:eventTypeId")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Delete a team event type" })
@@ -49,6 +49,17 @@ export class TeamsEventTypesRepository {
});
}
async getEventTypeByTeamIdAndSlug(teamId: number, eventTypeSlug: string) {
return this.dbRead.prisma.eventType.findUnique({
where: {
teamId_slug: {
teamId,
slug: eventTypeSlug,
},
},
});
}
async getTeamEventTypes(teamId: number) {
return this.dbRead.prisma.eventType.findMany({
where: {
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
@@ -27,7 +28,7 @@ import {
HttpStatus,
Logger,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -40,6 +41,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
})
@UseGuards(ApiAuthGuard, RolesGuard)
@DocsTags("Teams / Memberships")
@ApiHeader(API_KEY_HEADER)
export class TeamsMembershipsController {
private logger = new Logger("TeamsMembershipsController");
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -14,7 +15,7 @@ import { TeamsService } from "@/modules/teams/teams/services/teams.service";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Controller, UseGuards, Get, Param, ParseIntPipe, Delete, Patch, Post, Body } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -26,6 +27,7 @@ import { TeamOutputDto } from "@calcom/platform-types";
})
@UseGuards(ApiAuthGuard)
@DocsTags("Teams")
@ApiHeader(API_KEY_HEADER)
export class TeamsController {
constructor(private teamsService: TeamsService, private teamsRepository: TeamsRepository) {}
@@ -120,6 +120,11 @@ export class CreateTeamInput {
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ type: Boolean, default: true })
@ApiPropertyOptional({
type: Boolean,
default: true,
description:
"If you are a platform customer, don't pass 'false', because then team creator won't be able to create team event types.",
})
readonly autoAcceptCreator?: boolean = true;
}
@@ -30,7 +30,7 @@ export class TeamsRepository {
});
}
async getTeamMembersIds(teamId: number) {
async getTeamUsersIds(teamId: number) {
const teamMembers = await this.dbRead.prisma.membership.findMany({
where: {
teamId,
@@ -43,6 +43,22 @@ export class TeamsRepository {
return teamMembers.map((member) => member.userId);
}
async getTeamManagedUsersIds(teamId: number) {
const teamMembers = await this.dbRead.prisma.membership.findMany({
where: {
teamId,
user: {
isPlatformManaged: true,
},
},
});
if (!teamMembers || teamMembers.length === 0) {
return [];
}
return teamMembers.map((member) => member.userId);
}
async getTeamsUserIsMemberOf(userId: number) {
return this.dbRead.prisma.team.findMany({
where: {
@@ -1,7 +1,7 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { TimezonesService } from "@/modules/timezones/services/timezones.service";
import { Controller, Get } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiExcludeController, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { CityTimezones } from "@calcom/platform-libraries";
@@ -11,6 +11,8 @@ import { ApiResponse } from "@calcom/platform-types";
path: "/v2/timezones",
version: API_VERSIONS_VALUES,
})
// note(Lauris): controller used only internally by atoms
@ApiExcludeController()
@DocsTags("Timezones")
export class TimezonesController {
constructor(private readonly timezonesService: TimezonesService) {}
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { UserWithProfile } from "@/modules/users/users.repository";
@@ -15,7 +16,7 @@ import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
import { UserWebhooksService } from "@/modules/webhooks/services/user-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 as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { Webhook } from "@prisma/client";
import { plainToClass } from "class-transformer";
@@ -28,6 +29,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
})
@UseGuards(ApiAuthGuard)
@DocsTags("Webhooks")
@ApiHeader(API_KEY_HEADER)
export class WebhooksController {
constructor(
private readonly webhooksService: WebhooksService,
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,6 @@
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { TestingModule } from "@nestjs/testing";
import { EventType } from "@prisma/client";
import { Prisma } from "@calcom/prisma/client";
@@ -22,4 +20,8 @@ export class HostsRepositoryFixture {
},
});
}
async getEventTypeHosts(eventTypeId: number) {
return this.prismaReadClient.host.findMany({ where: { eventTypeId } });
}
}
@@ -11,7 +11,7 @@ import TypePage, { type PageProps as ClientPageProps } from "~/users/views/users
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(context);
return <TypePage {...props} />;
};
@@ -13,7 +13,7 @@ import type { PageProps as LegacyPageProps } from "~/users/views/users-type-publ
import LegacyPage from "~/users/views/users-type-public-view";
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(legacyCtx);
const { booking, isSEOIndexable = true, eventData, isBrandingHidden } = props;
@@ -31,7 +31,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
})),
],
};
const decodedParams = decodeParams(params);
const decodedParams = decodeParams(await params);
const metadata = await generateMeetingMetadata(
meeting,
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
@@ -52,7 +52,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(legacyCtx);
return <LegacyPage {...props} />;
@@ -11,7 +11,7 @@ import User, { type PageProps as ClientPageProps } from "~/users/views/users-pub
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(context);
return <User {...props} />;
};
@@ -13,7 +13,9 @@ import type { PageProps as LegacyPageProps } from "~/users/views/users-public-vi
import LegacyPage from "~/users/views/users-public-view";
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const props = await getData(
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
);
const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props;
const isOrg = !!profile?.organization;
@@ -31,7 +33,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
() => markdownStrippedBio,
false,
getOrgFullOrigin(entity.orgSlug ?? null),
`/${decodeParams(params).user}`
`/${decodeParams(await params).user}`
);
return {
@@ -45,7 +47,9 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const props = await getData(
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
);
return <LegacyPage {...props} />;
};
@@ -3,7 +3,7 @@ import { headers } from "next/headers";
import PageWrapper from "@components/PageWrapperAppDir";
export default async function BookingPageWrapperLayout({ children }: { children: React.ReactNode }) {
const h = headers();
const h = await headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
@@ -15,7 +15,7 @@ const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
export type ClientPageProps = UserTypePageProps | TeamTypePageProps;
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(context);
if ((props as TeamTypePageProps)?.teamId) return <TeamTypePage {...(props as TeamTypePageProps)} />;
return <UserTypePage {...(props as UserTypePageProps)} />;
@@ -17,7 +17,7 @@ export type OrgTypePageProps = UserTypePageProps | TeamTypePageProps;
const getData = withAppDirSsr<OrgTypePageProps>(getServerSideProps);
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(legacyCtx);
const { booking, isSEOIndexable = true, eventData, isBrandingHidden } = props;
@@ -36,7 +36,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
})),
],
};
const decodedParams = decodeParams(params);
const decodedParams = decodeParams(await params);
const metadata = await generateMeetingMetadata(
meeting,
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
@@ -56,7 +56,9 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
};
const ServerPage = async ({ params, searchParams }: PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const props = await getData(
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
);
if ((props as TeamTypePageProps)?.teamId) {
return <TeamTypePage {...(props as TeamTypePageProps)} />;
}
@@ -15,7 +15,7 @@ const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
export type ClientPageProps = UserPageProps | TeamPageProps;
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(context);
if ((props as TeamPageProps)?.team) return <TeamPage {...(props as TeamPageProps)} />;
return <UserPage {...(props as UserPageProps)} />;
@@ -18,7 +18,7 @@ export type OrgPageProps = UserPageProps | TeamPageProps;
const getData = withAppDirSsr<OrgPageProps>(getServerSideProps);
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(legacyCtx);
if ((props as TeamPageProps)?.team) {
@@ -30,7 +30,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
image: getOrgOrTeamAvatar(team),
},
};
const decodedParams = decodeParams(params);
const decodedParams = decodeParams(await params);
return {
...(await generateMeetingMetadata(
meeting,
@@ -78,7 +78,9 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
};
const ServerPage = async ({ params, searchParams }: PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const props = await getData(
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
);
if ((props as TeamPageProps)?.team) {
return <TeamPage {...(props as TeamPageProps)} />;
}
@@ -12,7 +12,7 @@ import type { Props } from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/i
import Page from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-meeting-view";
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const { isBrandingHidden, eventData } = await getData(context);
const profileName = eventData?.profile.name ?? "";
@@ -29,7 +29,7 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
})),
],
};
const decodedParams = decodeParams(params);
const decodedParams = decodeParams(await params);
const metadata = await generateMeetingMetadata(
meeting,
() => `${title} | ${profileName}`,
@@ -51,7 +51,9 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
const getData = withAppDirSsr<Props>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: _PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const props = await getData(
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
);
return <Page {...props} />;
};
@@ -10,7 +10,7 @@ import TypePage, { type PageProps as ClientPageProps } from "~/team/type-view";
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(context);
return <TypePage {...props} />;
};
@@ -12,7 +12,7 @@ import LegacyPage from "~/team/type-view";
import type { PageProps as LegacyPageProps } from "~/team/type-view";
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(legacyCtx);
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
@@ -29,7 +29,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
})),
],
};
const decodedParams = decodeParams(params);
const decodedParams = decodeParams(await params);
const metadata = await generateMeetingMetadata(
meeting,
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
@@ -51,7 +51,9 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const props = await getData(
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
);
return <LegacyPage {...props} />;
};
export default ServerPage;
@@ -10,7 +10,7 @@ import TeamPage, { type PageProps as ClientPageProps } from "~/team/team-view";
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(context);
return <TeamPage {...props} />;
};
@@ -14,7 +14,7 @@ import LegacyPage from "~/team/team-view";
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = await getData(
buildLegacyCtx(headers(), cookies(), params, searchParams)
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
);
const meeting = {
@@ -24,7 +24,7 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
image: getOrgOrTeamAvatar(team),
},
};
const decodedParams = decodeParams(params);
const decodedParams = decodeParams(await params);
const metadata = await generateMeetingMetadata(
meeting,
(t) => team.name || t("nameless_team"),
@@ -45,7 +45,9 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
const getData = withAppDirSsr<PageProps>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: _PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const props = await getData(
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
);
return <LegacyPage {...props} />;
};
export default ServerPage;
@@ -17,7 +17,7 @@ export const generateMetadata = async () => {
};
const Page = async () => {
// const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
// const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
// const userId = session?.user?.id;
// const orgId = session?.user?.org?.id;
// if (!userId || !orgId) {
@@ -18,7 +18,7 @@ export const generateMetadata = async () =>
);
const Page = async ({ params }: PageProps) => {
const parsed = querySchema.safeParse(params);
const parsed = querySchema.safeParse(await params);
if (!parsed.success) {
redirect("/bookings/upcoming");
}
@@ -8,8 +8,6 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
import EventTypes, { EventTypesCTA } from "~/event-types/views/event-types-listing-view";
export const generateMetadata = async () =>
@@ -19,14 +17,13 @@ export const generateMetadata = async () =>
);
const Page = async ({ params, searchParams }: PageProps) => {
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const session = await getServerSession({ req: context.req });
if (!session?.user?.id) {
redirect("/auth/login");
}
await ssrInit(context);
const t = await getTranslate();
return (
@@ -1,11 +1,17 @@
import { setUser as SentrySetUser } from "@sentry/nextjs";
import { cookies, headers } from "next/headers";
import React from "react";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import Shell from "@calcom/features/shell/Shell";
const Layout = ({ children }: { children: React.ReactNode }) => {
return (
<Shell withoutSeo={true} withoutMain={true}>
{children}
</Shell>
);
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const Layout = async ({ children }: { children: React.ReactNode }) => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (session?.user?.id) SentrySetUser({ id: session.user.id });
return <Shell withoutMain={true}>{children}</Shell>;
};
export default Layout;

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