Compare commits

...
119 Commits
Author SHA1 Message Date
GitHub Actions be525a27df chore: release v5.0.14
Post release / release (push) Canceled after 0s
2025-03-10 22:44:00 +00:00
Calcom BotandLingo.dev df037b008f feat: update translations via @lingodotdev (#19927)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-10 21:12:15 +00:00
amritandUdit Takkar 8fb4df4b8c fix: duplicate value found in common.json error (#19693)
* fix: duplicate value found in common.json error

* Remove subteam invitation translation from locales

* verify_email_email_button to verify_email_button

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-03-10 19:19:32 +00:00
6758654796 feat: round robin reset interval for teams (#19441)
* add component to set interval

* fix variable namings

* fetch data for given interval

* migration

* fix type errors in test

* test correct filtering for interval

* fix start end end times to interval

* fix type error

* pass rrResetInterval to getOrderedListOfLuckyUsers

* fix imports

* add translations

* add missing translation

* code clean up

* fix type errors

* fix type error

* fix import

* add translation

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-03-10 17:34:47 +00:00
Alex van AndelandMorgan Vernay 1e86f1046b chore: Fix build failure due to trpc inference depth (#19884)
* chore: Fix build failure due to trpc inference depth

* Also bump platform libraries typescript version

* fix: e2e test using unassigned variable

* chore: bump libraries to 0.0.142

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-03-10 17:06:05 +00:00
Eunjae LeeandAlex van Andel d3775cd920 fix: update filter styles on org member list (#19918)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-03-10 17:01:37 +00:00
Benny JooandAnik Dhabal Babu 3ab053bf17 chore: migrate oauth APIs to app router (#19824)
* migrate api/auth/oauth/me

* migrate the rest

* fix

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-10 17:56:53 +01:00
Benny Joo 4a4bee7140 chore: Remove typeform app (#19911)
* remove typeform app

* update yarn.lock
2025-03-10 16:54:20 +00:00
Benny Joo 9ba85ea0b2 chore: missing pieces to migrated apis (#19915)
* fix

* refactor
2025-03-10 15:24:03 +00:00
avnish100 d1d1472f3e fix: removed rounded top corners on webhook form (#19905) 2025-03-10 14:54:43 +00:00
Benny Jooandsean-brydon 8d18b3da38 chore: migrate saml/oidc APIs to App Router (#19515)
* migrate oidc

* migrate saml apis

* remove comment

* add force-dynamic

* fix

* fix

* refactor

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-03-10 14:36:11 +00:00
Benny Joo 631c5f7c53 migrate webhookTriggers and workflows apis (#19875) 2025-03-10 14:35:11 +00:00
f2ca2bdc83 fix: add badge for disabled workflow actions and templates in trial mode (#19675)
* add badge for trial mode

* fix tooltip issue

* fix typo

* add inactive team plan badge

* change to one salesforce request

* clean up

* fix type error

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2025-03-10 15:34:29 +01:00
Rajiv Sahal bb36807689 feat: update platform onboarding docs (#17941)
* update images

* restructure getting started for platform

* add steps on setting up a platform account

* PR feedback

* fixup

* add setup page

* move over setup to a separate page

* update images
2025-03-10 20:02:24 +05:30
Keith Williams 7ed32d3c77 perf: Reduce imports by moving bookings zod out of utils (#19869) 2025-03-10 13:54:05 +00:00
Morgan 5d84179345 refactor: circular dependencies platform packages (#19903)
* refactor: circular dependencies platform packages

* fixup! refactor: circular dependencies platform packages

* fixup! fixup! refactor: circular dependencies platform packages

* fix: circular deps in BaseCalProvider
2025-03-10 13:25:01 +00:00
cb437231ad fix: platform libraries barrel file extract event-types (#19846)
* fix: event-types

* chore: added "+ group link" tip (#19809)

* added + link tip

* nit

---------

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>

* Added fix to solve the id TypeError (#19810)

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>

* Revert "fix: prevent background scroll on opening timezone select menu (#19789)" (#19814)

This reverts commit 1a96149ce3.

* fix: Calendar events attached to workflow emails are missing meeting link (#19790)

* fixup! fix: Calendar events attached to workflow emails are missing meeting link (#19790)

* chore: fix type errors

* fix: wrong import in e2e file

* fix: tsconfig api v2 for /event-types

* fix: import and refactor

* fixed remaning type issues

* undo extra changes

* bump platform libs

* Update event-types.controller.e2e-spec.ts

* fix: test with seats enabled

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Harshit Singh <73997189+harshit078@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-03-10 12:34:53 +00:00
Somay Chauhan d47a5be659 fix: add documentaion to blockUnconfirmedBookingsInBooker (#19894)
* fix: add documentaion to `blockUnconfirmedBookingsInBooker`

* fix documentation of ConfirmationPolicy.type

* Update confirmation-policy.input.ts
2025-03-10 09:31:49 -03:00
sean-brydon e89211782c fix: calcom trpc circle deps slots handlers (#19896)
* remove validator file and include inside handler

* fixes slots and handler circle dep

* fix type in tests
2025-03-10 11:29:51 +00:00
sean-brydon fd454061e7 fix profile circle dep (#19892) 2025-03-10 11:05:14 +00:00
Rajiv SahalandMorgan Vernay 277ce1c244 refactor: extract slots from libraries barrel file (#19895)
* refactor: extract slots fn from libraries barrel file

* bump platform libs

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-03-10 10:33:29 +00:00
luzpaz 1848cd55b1 fix: fix playwright typos (#19794)
Found via `codespell -q 3 -S "*.svg,./apps/web/public/static/locales,./packages/app-store/stripepayment/lib/currencyOptions.ts,./packages/lib/freeEmailDomainCheck/freeEmailDomains.ts" -L afterall,atleast,datea,fo,incase,ist,nam,noone,notin,optionel,perview,pris,reccuring`
2025-03-10 07:39:58 +00:00
Anik Dhabal Babu b0e3cc4dd2 chore: add logs for debugging (#19851) 2025-03-10 03:34:52 +00:00
Morgan b3e344d226 chore: decouple calcom/ui/Tooltip from atoms (#19798)
* chore: decouple calcom/ui from atoms #1

* recover input from main

* fix: vite config dialog
2025-03-10 01:43:01 +00:00
Suyash Srivastava 810f3b834c fix:small typo with email (#19879) 2025-03-10 01:22:13 +00:00
Hariom Balhara bf1eacfd9b Ignore faulty org (#19825) 2025-03-10 00:58:48 +00:00
c5c654c879 feat: no-show confirmation does not pop up for single attendee (#19806)
* feat: no-show marking  confirmation does not pop up for single attendee bookings

* fix: Add toggle for no-show status

* simplify logic

* feat: Centralize no-show handling logic in BookingListItem

* Update BookingListItem.tsx

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-09 17:06:03 +00:00
sean-brydonandBenny Joo d1ce04edc1 chore: move miscellaneous small api pages to app dir api (#19699)
* version and verify-booking-token

* username route

* api/me

* logo route

* render email in dev

* link route

* future

* nope and geo location

* referal link and daily

* intercom route and dynamic variables

* intercom route and dynamic variables

* scim 2.0 and helpscout route

* app credentials

* api/book events

* fix daily import path in tests

* fix buildLegacyRequest generation

* fix type errors

* migrate the /teams/ routes

* move cron jobs

* fix daily import path in tests

* fix buildLegacyRequest generation

* fix type errors

* migrate the /teams/ routes

* move cron jobs

* Revert "api/book events"

This reverts commit 607a32fb5b754cad090c2d0cbf64a68f990e220e.

* mock next server NextResponse to fix daily video

* add default responder to teams api

* fix search params

* uses nextUrl.searchParams instead of new url

* uses nextUrl.searchParams instead of new url

* remove outdated api config

* remove app dir version of inbound dynamic variables

* restore pages version of inbound variables

* fix missing code from stupid cursor

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-03-09 11:56:56 -04:00
Benny JooandAnik Dhabal Babu 1f1862e526 chore: migrate two-factor APIs to app router (#19826)
* wip

* remove comment

* clean up

* clean up

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-09 15:40:27 +00:00
Benny Joo 63dfc3376c chore: remove empty path from middleware matcher (#19860) 2025-03-09 15:38:04 +00:00
Adam BashaandKeith Williams 3166db9bcf chore: update robots.txt to disallow /settings and /my-account (#19867)
* chore: update robots.txt to disallow /settings and /my-account

* Update robots.txt

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-09 08:53:34 +00:00
Vijay f8af9dc17e fix: failing tests during DST transition (#19862)
* make the test dst transition proof

* fix other test similarly
2025-03-09 00:43:42 +00:00
Benny Joo ae659b5176 chore: migrate forgot-password, reset-password, auth/setup apis to App Router (#19512)
* chore: migrate /api/auth/changepw

* migrate forgot-password and reset-password

* remove unneeded export

* rename request to req for easier review

* migrate auth/setup

* remove setup

* refactor

* remove redundant code

* fix

* remove changepw api and unused ui
2025-03-07 14:43:36 -07:00
Eunjae Lee 33226f268d fix: date range on /bookings (#19785) 2025-03-07 21:54:49 +01:00
sean-brydonandAnik Dhabal Babu 7e2a0d1a1b fix: calcom UI circle dep (#19839)
* editor circle dep fix

* circle dep fix in select

* fix barrel export types

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-07 09:26:26 -07:00
a385bd4d5f feat: team ooo read-only for non-admin (#19700)
* feat team ooo read-only for non-admin

* Update apps/web/playwright/out-of-office.e2e.ts

Co-authored-by: Younes Henni <youneshenniwrites@gmail.com>

* updated error msgs with t

* nit

* nit

---------

Co-authored-by: Younes Henni <youneshenniwrites@gmail.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-07 16:19:30 +00:00
Anik Dhabal Babu 4b4bc28f1d fix: Calendar events attached to workflow emails are missing meeting link (#19790) 2025-03-07 14:41:51 +00:00
Anik Dhabal Babu 316fba908b Revert "fix: prevent background scroll on opening timezone select menu (#19789)" (#19814)
This reverts commit 1a96149ce3.
2025-03-07 14:39:09 +00:00
d40d282912 Added fix to solve the id TypeError (#19810)
Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-07 14:26:36 +00:00
e81df6fdeb chore: added "+ group link" tip (#19809)
* added + link tip

* nit

---------

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-07 14:23:51 +00:00
Hariom Balhara 76420411b9 fix: fixes the crash when empty users or hosts array is passed to _getDelegationCredentialsMapPerUser (#19834)
* fix crash when no users are there

* typo fix
2025-03-07 13:11:07 +00:00
Keith Williams 7ec1617dc2 chore: Update CODEOWNERS for package.json files (#19831) 2025-03-07 11:02:33 +00:00
Rajiv Sahal 870cadd81f fix: dry run improvements for v2 (#19196)
* add dry run param for v2 getSlots

* fixup

* revert changes

* dont reserve a slot for booking dry run

* accept dry run prop as a parameter instead of computing

* fix merge conflicts

* fixup

* dont reserve slots for booking dry run
2025-03-07 14:49:54 +05:30
amrit 04c581a25e docs: Update README (#19815) 2025-03-07 10:32:33 +05:30
Syed Ali Shahbaz 5ef131333d update SAML guide (#19778) 2025-03-07 09:00:53 +05:30
GitHub Actions c262220f0f chore: release v5.0.13
Post release / release (push) Canceled after 0s
2025-03-07 00:38:07 +00:00
Morgan b5e08ea80f fix: api v2 create event-type with delegation credentials (#19797) 2025-03-06 19:46:46 +02:00
Joe Au-Yeung 00ee1ef441 refactor: Salesforce - account query based on website (#19799)
* Refactor account lookup to include website prefixes

* Type fix
2025-03-06 21:53:49 +05:30
Rajiv Sahal 6cdc9c9f59 fix invalid access token bug (#19793) 2025-03-06 14:51:39 +00:00
Pranav Rajveerandamrit 56d1998f0f chore: rename virtual-queue to router-position (#19774)
Co-authored-by: amrit <iamamrit27@gmail.com>
2025-03-06 19:32:30 +05:30
Udit Takkar d59fdf21fd feat: display country code from IP (#19788)
* feat: display country code from IP

* chore: remove

* fix: unit test
2025-03-06 19:16:41 +05:30
suraj 1a96149ce3 fix: prevent background scroll on opening timezone select menu (#19789) 2025-03-06 13:29:50 +00:00
Eunjae Lee d2bc81758c feat: add booking id & booking link columns to /insights/routing (#19697)
* feat: add booking id & booking link columns to /insights/routing

* update style

* update i18n

* update i18n keys

* update host

* useCopy instead of raw navigator.clipboard
2025-03-06 10:24:50 +00:00
Alex van Andel 0565e23818 refactor: Remove intervalLimits from @calcom/lib and export directly (#19710)
* refactor: Remove intervalLimits from @calcom/lib and export directly

* Tackle other places that use parseBookingLimit/parseDurationLimit

* More type fixups that were hidden by previous fails

* Fixed up booking-limits file

* Remove server-only
2025-03-05 19:32:53 +00:00
GitHub Actions 34cff214dd chore: release v5.0.12
Post release / release (push) Canceled after 0s
2025-03-05 18:40:35 +00:00
Alex van Andel f7af19b7cb chore: Bump node to 20 in APIv2 Dockerfile (#19770) 2025-03-05 18:19:29 +00:00
Bailey Pumfleetandsnyk-bot 6849638b1f fix: apps/api/v2/package.json & apps/api/v2/.snyk to reduce vulnerabilities (#19755)
The following vulnerabilities are fixed with a Snyk patch:
- https://snyk.io/vuln/npm:semver:20150403

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2025-03-05 18:07:55 +00:00
Joe Au-Yeung 07969f5287 perf: Add CrmServiceMap (#19718)
* Generate crm apps map

* Remove .tsx file

* Use `CrmServiceMap` in `getCrm`

* Remove unused types

* Type fixes

* Type fixes
2025-03-05 14:58:02 -03:00
GitHub Actions 44a6ceff3f chore: release v5.0.12 2025-03-05 17:52:37 +00:00
Vijay 0e4309777b fix: make strict bottom position of DateRangePicker optional (#19768)
* make strictbottom optional

* chore
2025-03-05 17:26:10 +00:00
Keith Williams b8f0741923 chore: Update CODEOWNERS to have @calcom/Foundation on Platform line items (#19769) 2025-03-05 10:01:34 -07:00
nizzy ba1ede2e31 fix: plain z index (#19767) 2025-03-05 16:15:01 +00:00
Udit TakkarandBailey Pumfleet b74a49f31d fix: use impersonatedById (#19682)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2025-03-05 15:43:25 +00:00
Tushar Bhatt 31cd77345d all changes (#19720) 2025-03-05 15:41:54 +00:00
Anik Dhabal Babu 2be9ff7622 fix: hubspot not working for team events (#19761) 2025-03-05 15:16:49 +00:00
Alex van Andel 96a5abe6c3 chore: Fix types of app-store to prevent type drilling (#19732) 2025-03-05 12:06:01 -03:00
sean-brydonandKeith Williams 4cdb8f3bae feat: make calcom UI dumb again (#19658)
* remove create button for teams from calcom ui

* migrate timezone select to features

* remove trpc from phone and storybook provider

* new-yarn.lock

* fix imports + test file for select

* fix platform timezone select to use Raw timezone select

* fix timezone type import

* use correct select in timezone-select.tsx - needs trpc version

* fix lock and remove test-utils

* fix log file

* Updated yarn.lock

* Fixed types

* Fixed tests

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-05 11:58:52 -03:00
Shivansh KumarandAnik Dhabal Babu 1b111c8add fix(payments): add return_url for Alipay PaymentIntent confirmation; fixes #19138 (#19153)
* Update README.md

* fix(payments): add return_url for Alipay PaymentIntent confirmation; fixes #19138

* Fix: update confirmPayment to use confirmParams and resolve TS errors

* Update Payment.tsx

* Update

* fix build error

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-05 11:57:43 -03:00
luzpazandTushar Bhatt be1377e9af fix: typos in apps/api (#19762)
Found via codespell

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-05 14:29:11 +00:00
Alex van Andel 7226370427 chore: Bump .nvmrc node version to node 20 (#19760) 2025-03-05 13:50:06 +00:00
Morgan 524c0d81a7 chore: rename DWD to DelegationCredential (#19744)
* Revert "Revert "chore: rename DWD to DelegationCredential (#19703)" (#19734)"

This reverts commit 340b5ab061.

* chore: fix schema and types for now

* fix: domainWideDelegationCredentialId error type
2025-03-05 10:42:20 -03:00
Udit TakkarandPeer Richelsen f008a28499 fix: open cal video url on new page when ready (#19683)
* fix: open cal video url on new page when ready

* fix: open on new tab

* chore: change logic

* chore: add condition for embed

* chore: simplify the code

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-03-05 18:27:06 +05:30
Keith Williams 381f2781a3 perf: remove atoms monorepo imports (#19688)
* perf: Remove atoms/monorepo barrel exports

* Removed more exports

* completely removed the monorepo file

* fixed types

* fixing tests

* fixing package errors

* Pushing latest

* Fixed atoms build

* hack to fix type

* Fixed web build
2025-03-05 09:49:13 -03:00
Alex van Andel 6f62c6ad5d refactor: Typescript fixes and updates for bumping ts version to 5.8.2 (#19690)
* Typescript fixes and updates for bumping ts version to 5.8.2

* Different TS versions do see an error, so @ts-ignore'ing

* Attempt this fix for ignoring createDecipher/createCipher fns

* Remove @ts-expect-error
2025-03-05 09:17:08 -03:00
Alex van Andel 1545a8ec82 chore: Upgrade to node v20 as v18 is EOL next month (#19733)
* chore: Upgrade to node v20 as v18 is EOL next month

* Run tests using node 20
2025-03-05 13:34:17 +02:00
Muhammad Aiman Sulaiman 58d7e59b38 feat: Updates to HitPay Payment App (#19737)
* feat: add hitpay embed feature for cal embed

feat: update hitpay app description

fix: fixed customers being able to exceed bookings in the same timeslot when using HitPay

fix: fixed callback issue in cal.com using hitpay app when not receiving payment responses

* fix: fixed type-check error
2025-03-05 11:29:38 +00:00
Eunjae Lee d79a5c3adf fix: search on team availiability (for non-org users) (#19738) 2025-03-05 10:56:03 +00:00
Morgan 340b5ab061 Revert "chore: rename DWD to DelegationCredential (#19703)" (#19734)
This reverts commit 7a9ddf2194.
2025-03-05 10:30:23 +00:00
Anik Dhabal Babu fa58fe6563 fix: tab item is not translated (#19735) 2025-03-05 09:28:45 +00:00
Benny Joo 251121a76f chore: write defaultResponder for App Router APIs (#19727)
* create defaultResponderForAppDir

* rename all usages

* finish

* fix
2025-03-05 09:12:07 +00:00
Rajiv Sahal 75627cb097 fix: booking fields not saving properly for team events (#19702) 2025-03-05 05:38:07 +00:00
amrit f9780fbbd2 fix: auto submission upon uploading photo (#19715)
* fix: auto submission upon uploading photo

* set image source from server response

* get rid of extra network request by removing refetch
2025-03-05 05:34:02 +00:00
luzpaz 13649d3bf3 fix: typos in packages directory (#19574)
Found via codespell
2025-03-05 05:12:40 +00:00
Benny Joo 65898f29eb perf: dynamic import for sentry CaptureException (#19730) 2025-03-04 23:07:20 +00:00
Calcom BotandLingo.dev 11267e9359 feat: update translations via @lingodotdev (#19729)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-04 21:40:29 +00:00
sean-brydonandBailey Pumfleet d38b35b626 chore: remove storybook (#19659)
* remove storybook

* remove storybook trpc provider

* add user-event

* fix lockk file

---------

Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2025-03-04 14:14:56 -07:00
Lauris Skraucis fff521e171 fix: v2 managed user attendee bookings (#19716)
* Revert "Revert "fix: v2 managed user attendee bookings (#19612)" (#19635)"

This reverts commit 2600a6c3d5.

* fix: make guests optional
2025-03-04 18:08:21 +01:00
Vijay fd6666ebad feat: improved ooo search (#19704)
* improved ooo search

* chore
2025-03-04 13:45:00 +00:00
Anik Dhabal Babu f2fa1248eb fix (#19692) 2025-03-04 18:21:39 +05:30
Morgan 7a9ddf2194 chore: rename DWD to DelegationCredential (#19703)
* chore: rename DWD to DelegationCredential

* fixup! chore: rename DWD to DelegationCredential

* fixup! fixup! chore: rename DWD to DelegationCredential

* fixup! Merge branch 'main' into rename-domain-wide-delegation

* fixup! fixup! Merge branch 'main' into rename-domain-wide-delegation

* fixup! fixup! fixup! Merge branch 'main' into rename-domain-wide-delegation
2025-03-04 09:45:39 -03:00
Anik Dhabal Babu a39b5e47a7 fix: member filter fix (#19707) 2025-03-04 12:45:27 +00:00
Morgan ae8180e7c2 chore: improve validate hosts api v2 team event-types (#19696)
* chore: improve validate hosts api v2 team event-types

* fixup! chore: improve validate hosts api v2 team event-types
2025-03-04 08:55:40 +00:00
Morgan 72832354e6 fix: api v2 axiom logs exceed column size (#19636) 2025-03-04 05:40:36 +00:00
Vachmara df598bf281 feat: improve Intercom app (#19687)
* fix: don't POST to cal.com/[user]/[type]

* fix: return html with cal calendar embed

* fix: intercom app team events types

* feat: improve Intercom UX integration

* Merge branch 'main' of https://github.com/calcom/cal.com
2025-03-04 10:48:31 +05:30
GitHub Actions a3ce957f00 chore: release v5.0.11
Post release / release (push) Canceled after 0s
2025-03-03 23:30:41 +00:00
Keith Williams 7701edb0bb chore: TS config - ignore node_modules in subdirectories (#19680)
* chore: TS config - ignore node_modules in subdirectories

* Update tsconfig.json

* Applied the pattern to the entire monorepo
2025-03-03 15:53:28 -03:00
Benny Joo efd427831d chore: early return for next.js static assets (#19678) 2025-03-03 15:35:42 -03:00
bec83b2342 chore: remove TRPC barrel exports (#19598)
* remove barrel export for client

* fix more imports

* remove server barrel

* remove more barrel exporst and fix tssconfig

* fix types

* fix imports for removed barrels

* fig ssg createserverside helpers

* restore lock

* new yarn lock

* remove barel exports

* remove barrel

* replace client with react in tsconfig

* fix yarn lock

* Update exports for trpc to not be types

* add ENDPOINTS export to the correct location

* other exports fixes lost due to barrel

* fix client error export

* more imports

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-03 15:33:32 -03:00
Ash DavisandAnik Dhabal Babu 5a6a13b954 fix: allow cancelledBy query param to work on api v1 (#19268)
* fix: cancelledby to work on api v1

* fix: scheduleId to work with hosts in eventtype api v1

* Update event-type.ts

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-03 18:19:09 +00:00
Ash Davisandamrit 340f15fea5 fix: scheduleId to work on event type host assignment via patch api v1 (#19546)
Co-authored-by: amrit <iamamrit27@gmail.com>
2025-03-03 18:18:06 +00:00
Anik Dhabal Babu f572a2b9c2 fix: some flaky e2e tests (#19648)
* fix: some flaky e2e tests

* update

* update
2025-03-03 21:55:21 +05:30
MorganandSomay Chauhan ee9c73476b chore: add docs to /v2/organizations/:orgId/organizations endpoints (#19668)
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2025-03-03 16:03:32 +00:00
Keith Williams 78d06c6d01 refactor: Move classNames from @calcom/lib to @calcom/ui (#19674)
* refactor: Move classNames from @calcom/lib to @calcom/ui

* Import fix

* Removed extra import of classNames

* Removed tailwind-merge from @calcom/lib
2025-03-03 15:54:33 +00:00
Eunjae Lee 3d3a9c0dfb fix: use portal for DataTableSelectionBar (#19672) 2025-03-03 15:42:38 +00:00
Tushar Bhatt ed06050fda fix UI on mobile (#19673) 2025-03-03 15:24:37 +00:00
amrit fd6effa6ff feat: booking redirect page url is now has uid in it's parameters (#19522)
* feat: booking redirect page is now w UID

* refactor: use getBookingRedirectExtraParams

* refactor: remove uncessary const result

* uid isnt the first param anymore
2025-03-03 09:47:32 -05:00
Morgan 2889893804 refactor: import UseIsPlatform directly instead of barrel file (#19670)
* refactor: import useIsPlatform directly

* fixup! refactor: import useIsPlatform directly

* fixup! fixup! refactor: import useIsPlatform directly

* fixup! fixup! fixup! refactor: import useIsPlatform directly
2025-03-03 14:26:50 +00:00
Rajiv Sahal 3c035ab636 chore: add hooks section to v2 docs (#19651)
* add custom hooks page
2025-03-03 14:18:39 +00:00
sean-brydon c1ccd62f98 use profileUsername (#19667) 2025-03-03 12:28:40 +00:00
Eunjae Lee 0e5775a087 fix: adjust table height on /bookings (#19662) 2025-03-03 11:00:38 +00:00
amritandUdit Takkar 7e83f1a59a fix locale (#19665)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-03-03 10:52:31 +00:00
Eunjae LeeandUdit Takkar 12724ef894 fix: show selected filter values first in single- & multi-select filters (#19620)
* fix: show selected filter values first in single- & multi-select filters

* fix type errors

* fix type error

* fix type error

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-03-03 10:28:45 +00:00
Somay ChauhanandPeer Richelsen 6244b5a2c0 feat: added Ability to set organizer's default app as a valid input location in api/v2 (#19549)
* feat: added Ability to set organizer's default app as a valid input location in api/v2

* only allow organizers default for team events

* fixed for atoms and added unit and e2e tests

* Update organizations-event-types.e2e-spec.ts

* Update transformApiEventTypeForAtom.ts

* published platform-libraries@0.0.124

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-03-03 09:01:24 +00:00
Anik Dhabal Babu 615ef93572 fix: remove ‘add guests’ from the dropdown if it’s disabled in the booking questions (#19649)
* fix: remove ‘add guests’ from the dropdown if it’s disabled in the booking questions

* fix: remove dd guests' from the dropdown if it's
disabled in the booking questions
2025-03-03 05:06:51 +00:00
Anik Dhabal Babu 22f1060ed6 fix: fix: Incorrect Attendee Shown When Cancelling a Seated Event (#19647) 2025-03-03 05:05:36 +00:00
amrit 51a39571aa fix height (#19656) 2025-03-03 04:38:56 +00:00
Keith Williams 7180eb067a refactor: Move @calcom/core to @calcom/lib (#19655)
* refactor: Move @calcom/core to @calcom/lib

* Merging imports

* Clean up

* Ignoreing type error for now
2025-03-02 23:02:35 -03:00
Alex van AndelandOmar López 1cfd94d293 fix: Re-instate new slot logic now it's A/B tested in production (#19605)
* fix: Re-instate new slot logic now it's well tested

* Fixup getAggregatedAvailability

* Changes to code-owners to reflect removed/moved code

* Move more files around.

* Fix failed type

---------

Co-authored-by: Omar López <zomars@me.com>
2025-03-02 23:32:31 +00:00
Calcom BotandLingo.dev 4bab8c2c32 feat: update translations via @lingodotdev (#19654)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-02 17:53:42 -05:00
1096 changed files with 10823 additions and 22197 deletions
+5 -5
View File
@@ -1,4 +1,5 @@
/apps/api/**/* @calcom/Platform
/**/package.json @calcom/Foundation
/apps/api/**/* @calcom/Platform @calcom/Foundation
/deploy/**/* @calcom/Foundation
/infra/**/* @calcom/Foundation
/packages/app-store/applecalendar/**/* @calcom/Foundation
@@ -12,12 +13,11 @@
/packages/app-store/larkcalendar/**/* @calcom/Foundation
/packages/app-store/office365calendar/**/* @calcom/Foundation
/packages/app-store/zohocalendar/**/* @calcom/Foundation
/packages/core/getAggregatedAvailability/index.ts @calcom/Foundation
/packages/core/getAggregateWorkingHours.ts @calcom/Foundation
/packages/core/getUserAvailability.ts @calcom/Foundation
/packages/features/bookings/lib/**/* @calcom/Foundation
/packages/lib/getAggregatedAvailability.ts @calcom/Foundation
/packages/lib/getUserAvailability.ts @calcom/Foundation
/packages/lib/server/getLuckyUser.ts @calcom/Foundation
/packages/lib/slots.ts @calcom/Foundation
/packages/platform/**/* @calcom/Platform
/packages/platform/**/* @calcom/Platform @calcom/Foundation
/packages/prisma/**/* @calcom/Foundation
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
+1 -1
View File
@@ -14,7 +14,7 @@ description: "Run yarn install with node_modules linker and cache enabled"
inputs:
node_version:
required: false
default: v18.x
default: v20.x
runs:
using: "composite"
+1 -1
View File
@@ -1 +1 @@
v18
20
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
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"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+9
View File
@@ -0,0 +1,9 @@
undecided:
- calcom-monorepo
- "@calcom/api-v2"
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+14 -13
View File
@@ -172,24 +172,25 @@ yarn dx
```
#### Development tip
1. Add `export NODE_OPTIONS=“--max-old-space-size=16384”` to your shell script to increase the memory limit for the node process. Alternatively, you can run this in your terminal before running the app. Replace 16384 with the amount of RAM you want to allocate to the node process.
Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
Where {level} can be one of the following:
2. Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
Where {level} can be one of the following:
`0` for silly \
`1` for trace \
`2` for debug \
`3` for info \
`4` for warn \
`5` for error \
`6` for fatal
`0` for silly \
`1` for trace \
`2` for debug \
`3` for info \
`4` for warn \
`5` for error \
`6` for fatal
When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works:
When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works:
The logger will include all logs that are at the specified level or higher. For example: \
The logger will include all logs that are at the specified level or higher. For example: \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \
```sh
echo 'NEXT_PUBLIC_LOGGER_LEVEL=3' >> .env
+2 -2
View File
@@ -113,7 +113,7 @@ We use it in several ways, but mainly, we first import the auto-generated schema
We have some shared validations which several resources require, like baseApiParams which parses apiKey in all requests, or querIdAsString or TransformParseInt which deal with the id's coming from req.query.
- **[*]BaseBodyParams** that omits any values from the model that are too sensitive or we don't want to pick when creating a new resource like id, userId, etc.. (those are gotten from context or elswhere)
- **[*]BaseBodyParams** that omits any values from the model that are too sensitive or we don't want to pick when creating a new resource like id, userId, etc.. (those are gotten from context or elsewhere)
- **[*]Public** that also omits any values that we don't want to expose when returning the model as a response, which we parse against before returning all resources.
@@ -208,7 +208,7 @@ Add this command: `./scripts/vercel-deploy.sh`
See `scripts/vercel-deploy.sh` for more info on how the deployment is done.
> _❗ IMORTANT: If you're forking the API repo you will need to update the URLs in both the main repo [`.gitmodules`](https://github.com/calcom/cal.com/blob/main/.gitmodules#L7) and this repo [`./scripts/vercel-deploy.sh`](https://github.com/calcom/api/blob/main/scripts/vercel-deploy.sh#L3) ❗_
> _❗ IMPORTANT: If you're forking the API repo you will need to update the URLs in both the main repo [`.gitmodules`](https://github.com/calcom/cal.com/blob/main/.gitmodules#L7) and this repo [`./scripts/vercel-deploy.sh`](https://github.com/calcom/api/blob/main/scripts/vercel-deploy.sh#L3) ❗_
## Environment variables
+2 -1
View File
@@ -8,7 +8,8 @@ import {
_TeamModel,
_UserModel,
} from "@calcom/prisma/zod";
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";
import { iso8601 } from "@calcom/prisma/zod-utils";
import { extendedBookingCreateBody } from "@calcom/prisma/zod/custom/booking";
import { schemaQueryUserId } from "./shared/queryUserId";
@@ -22,6 +22,7 @@ const recurringEventInputSchema = z.object({
const hostSchema = _HostModel.pick({
isFixed: true,
userId: true,
scheduleId: true,
});
export const childrenSchema = z.object({
+2 -2
View File
@@ -113,7 +113,7 @@ const schemaUserEditParams = z.object({
});
// @note: These are the values that are editable via PATCH method on the user Model,
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
// merging both BaseBodyParams with RequiredParams, and omitting whatever we want at the end.
const schemaUserCreateParams = z.object({
email: emailSchema.toLowerCase(),
@@ -137,7 +137,7 @@ const schemaUserCreateParams = z.object({
});
// @note: These are the values that are editable via PATCH method on the user Model,
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
// merging both BaseBodyParams with RequiredParams, and omitting whatever we want at the end.
export const schemaUserEditBodyParams = schemaUserBaseBodyParams
.merge(schemaUserEditParams)
.omit({})
+1 -2
View File
@@ -10,7 +10,6 @@ const nextConfig = {
},
transpilePackages: [
"@calcom/app-store",
"@calcom/core",
"@calcom/dayjs",
"@calcom/emails",
"@calcom/features",
@@ -47,7 +46,7 @@ const nextConfig = {
async rewrites() {
return {
afterFiles: [
// This redirects requests recieved at / the root to the /api/ folder.
// This redirects requests received at / the root to the /api/ folder.
{
source: "/v:version/:rest*",
destination: "/api/v:version/:rest*",
-1
View File
@@ -24,7 +24,6 @@
},
"dependencies": {
"@calcom/app-store": "*",
"@calcom/core": "*",
"@calcom/dayjs": "*",
"@calcom/emails": "*",
"@calcom/features": "*",
+1 -1
View File
@@ -1,7 +1,7 @@
import type { NextApiRequest } from "next";
import { z } from "zod";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
import { getUserAvailability } from "@calcom/lib/getUserAvailability";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
+11 -3
View File
@@ -38,6 +38,12 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* schema:
* type: string
* description: The reason for cancellation of the booking
* - in: query
* name: cancelledBy
* required: false
* schema:
* type: string
* description: The email of who cancelled the booking
* tags:
* - bookings
* responses:
@@ -64,15 +70,17 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* description: User not found
*/
async function handler(req: NextApiRequest) {
const { id, allRemainingBookings, cancellationReason } = schemaQueryIdParseInt
.merge(bookingCancelSchema.pick({ allRemainingBookings: true, cancellationReason: true }))
const { id, allRemainingBookings, cancellationReason, cancelledBy } = schemaQueryIdParseInt
.merge(
bookingCancelSchema.pick({ allRemainingBookings: true, cancellationReason: true, cancelledBy: true })
)
.parse({
...req.query,
allRemainingBookings: req.query.allRemainingBookings === "true",
});
// Normalizing for universal handler
req.body = { id, allRemainingBookings, cancellationReason };
req.body = { id, allRemainingBookings, cancellationReason, cancelledBy };
return await handleCancelBooking(req);
}
@@ -112,7 +112,7 @@ export async function patchHandler(req: NextApiRequest) {
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaBookingEditBodyParams>) {
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
if (body.userId && !isSystemWideAdmin && !isOrganizationOwnerOrAdmin) {
// Organizer has to be a cal user and we can't allow a booking to be transfered to some other cal user's name
// Organizer has to be a cal user and we can't allow a booking to be transferred to some other cal user's name
throw new HttpError({
statusCode: 403,
message: "Only admin can change the organizer of a booking",
@@ -1,9 +1,11 @@
import type { NextApiRequest } from "next";
import { getRecordingsOfCalVideoByRoomName } from "@calcom/core/videoClient";
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import {
getRecordingsOfCalVideoByRoomName,
getDownloadLinkOfCalVideoByRecordingId,
} from "@calcom/lib/videoClient";
import prisma from "@calcom/prisma";
import type { RecordingItemSchema } from "@calcom/prisma/zod-utils";
import type { PartialReference } from "@calcom/types/EventManager";
@@ -1,11 +1,11 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import {
getTranscriptsAccessLinkFromRecordingId,
checkIfRoomNameMatchesInRecording,
} from "@calcom/core/videoClient";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
} from "@calcom/lib/videoClient";
import prisma from "@calcom/prisma";
import type { PartialReference } from "@calcom/types/EventManager";
@@ -1,8 +1,8 @@
import type { NextApiRequest } from "next";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/lib/videoClient";
import prisma from "@calcom/prisma";
import type { PartialReference } from "@calcom/types/EventManager";
@@ -2,7 +2,7 @@ import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import type { z } from "zod";
import { getCalendarCredentialsWithoutDwd, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
@@ -185,7 +185,7 @@ async function verifyCredentialsAndGetId({
currentCredentialId: number | null;
}) {
if (parsedBody.integration && parsedBody.externalId) {
const calendarCredentials = getCalendarCredentialsWithoutDwd(userCredentials);
const calendarCredentials = getCalendarCredentialsWithoutDelegation(userCredentials);
const { connectedCalendars } = await getConnectedCalendars(
calendarCredentials,
@@ -1,6 +1,6 @@
import type { NextApiRequest } from "next";
import { getCalendarCredentialsWithoutDwd, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
@@ -82,7 +82,7 @@ async function postHandler(req: NextApiRequest) {
message: "Bad request, credential id invalid",
});
const calendarCredentials = getCalendarCredentialsWithoutDwd(userCredentials);
const calendarCredentials = getCalendarCredentialsWithoutDelegation(userCredentials);
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, [], parsedBody.externalId);
+1 -1
View File
@@ -124,7 +124,7 @@ async function defaultScheduleId<T extends DefaultScheduleIdEventTypeBase>({
}, {} as { [x: number]: number | null });
return eventTypes.map((eventType) => {
// realistically never happens, userId should't be null on personal event types.
// realistically never happens, userId shouldn't be null on personal event types.
if (!eventType.userId) return eventType;
return {
...eventType,
@@ -28,7 +28,7 @@ import { membershipIdSchema } from "~/lib/validations/membership";
* - memberships
* responses:
* 201:
* description: OK, membership removed successfuly
* description: OK, membership removed successfully
* 400:
* description: Bad request. Membership id is invalid.
* 401:
@@ -31,7 +31,7 @@ import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId";
* - users
* responses:
* 201:
* description: OK, user removed successfuly
* description: OK, user removed successfully
* 400:
* description: Bad request. User id is invalid.
* 401:
@@ -5,11 +5,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test, vi, afterEach } from "vitest";
import { buildBooking } from "@calcom/lib/test/builder";
import {
getRecordingsOfCalVideoByRoomName,
getDownloadLinkOfCalVideoByRecordingId,
} from "@calcom/core/videoClient";
import { buildBooking } from "@calcom/lib/test/builder";
} from "@calcom/lib/videoClient";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -22,7 +22,7 @@ type CustomNextApiResponse = NextApiResponse & Response;
const adminUserId = 1;
const memberUserId = 10;
vi.mock("@calcom/core/videoClient", () => {
vi.mock("@calcom/lib/videoClient", () => {
return {
getRecordingsOfCalVideoByRoomName: vi.fn(),
getDownloadLinkOfCalVideoByRecordingId: vi.fn(),
@@ -5,11 +5,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test, vi, afterEach } from "vitest";
import { buildBooking } from "@calcom/lib/test/builder";
import {
getTranscriptsAccessLinkFromRecordingId,
checkIfRoomNameMatchesInRecording,
} from "@calcom/core/videoClient";
import { buildBooking } from "@calcom/lib/test/builder";
} from "@calcom/lib/videoClient";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -19,7 +19,7 @@ import handler from "../../../../../../pages/api/bookings/[id]/transcripts/[reco
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
vi.mock("@calcom/core/videoClient", () => {
vi.mock("@calcom/lib/videoClient", () => {
return {
getTranscriptsAccessLinkFromRecordingId: vi.fn(),
checkIfRoomNameMatchesInRecording: vi.fn(),
@@ -5,8 +5,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test, vi, afterEach } from "vitest";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient";
import { buildBooking } from "@calcom/lib/test/builder";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/lib/videoClient";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -16,7 +16,7 @@ import handler from "../../../../../pages/api/bookings/[id]/transcripts/_get";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
vi.mock("@calcom/core/videoClient", () => {
vi.mock("@calcom/lib/videoClient", () => {
return {
getAllTranscriptsAccessLinkFromRoomName: vi.fn(),
};
@@ -81,6 +81,7 @@ describe("POST /api/selected-calendars", () => {
userId: 444444,
id: "xxx-xxx",
eventTypeId: null,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
googleChannelId: null,
googleChannelKind: null,
@@ -123,6 +124,7 @@ describe("POST /api/selected-calendars", () => {
googleChannelResourceId: null,
googleChannelResourceUri: null,
googleChannelExpiration: null,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
eventTypeId: null,
error: null,
+1 -1
View File
@@ -18,5 +18,5 @@
"../../../packages/types/*.d.ts",
"../../../packages/types/next-auth.d.ts"
],
"exclude": ["node_modules", "templates", "auth"]
"exclude": ["**/node_modules/**", "templates", "auth"]
}
+12
View File
@@ -0,0 +1,12 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.25.1
ignore: {}
# patches apply the minimum changes required to fix a vulnerability
patch:
'npm:semver:20150403':
- '@calcom/platform-libraries-0.0.2 > @calcom/features > @calcom/atoms > rollup-plugin-node-builtins > browserify-fs > levelup > semver':
patched: '2025-03-05T12:50:30.654Z'
id: 'npm:semver:20150403'
path: >-
@calcom/platform-libraries-0.0.2 > @calcom/features > @calcom/atoms >
rollup-plugin-node-builtins > browserify-fs > levelup > semver
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:18-alpine as build
FROM node:20-alpine AS build
ARG DATABASE_DIRECT_URL
ARG DATABASE_URL
+8 -5
View File
@@ -24,13 +24,15 @@
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch",
"prisma": "yarn workspace @calcom/prisma prisma",
"generate-schemas": "yarn prisma generate && yarn prisma format",
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts"
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts",
"prepare": "yarn run snyk-protect",
"snyk-protect": "snyk-protect"
},
"dependencies": {
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.123",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.142",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -50,6 +52,7 @@
"@sentry/nestjs": "^8.37.1",
"@sentry/node": "^8.8.0",
"@sentry/profiling-node": "^8.37.1",
"@snyk/protect": "latest",
"body-parser": "^1.20.2",
"bull": "^4.12.4",
"class-transformer": "^0.5.1",
@@ -86,7 +89,6 @@
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.10",
"@types/luxon": "^3.3.7",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^3.0.13",
"@types/supertest": "^2.0.12",
"jest": "^29.7.0",
@@ -99,9 +101,10 @@
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.0",
"typescript": "^4.9.4"
"typescript": "^5.8.2"
},
"prisma": {
"schema": "../../../packages/prisma/schema.prisma"
}
},
"snyk": true
}
@@ -1,17 +1,39 @@
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { BillingModule } from "@/modules/billing/billing.module";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, RedisModule, TokensModule, BillingModule],
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository, ApiKeysRepository],
imports: [
PrismaModule,
RedisModule,
TokensModule,
BillingModule,
UsersModule,
EventTypesModule_2024_04_15,
SchedulesModule_2024_04_15,
EventTypesModule_2024_06_14,
],
providers: [
TokensRepository,
OAuthFlowService,
OAuthClientRepository,
ApiKeysRepository,
OAuthClientUsersService,
PlatformBookingsService,
],
controllers: [BookingsController_2024_04_15],
})
export class BookingsModule_2024_04_15 {}
@@ -114,7 +114,6 @@ describe("Bookings Endpoints 2024-04-15", () => {
optionValue: "",
},
notes: "test",
guests: [],
};
const body: CreateBookingInput_2024_04_15 = {
@@ -145,6 +144,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
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);
createdBooking = responseBody.data;
});
@@ -244,6 +244,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
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);
createdBooking = responseBody.data;
});
@@ -269,7 +270,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
optionValue: "",
},
notes: "test",
guests: [],
guests: ["someone@example.com"],
};
const body: CreateBookingInput_2024_04_15 = {
@@ -301,6 +302,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
expect(responseBody.data.metadata).toEqual(newBookingMetadata);
expect(responseBody.data.responses).toEqual(bookingResponses);
createdBooking = responseBody.data;
});
@@ -4,6 +4,7 @@ import { MarkNoShowInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/mark
import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output";
import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
@@ -99,7 +100,8 @@ export class BookingsController_2024_04_15 {
private readonly oAuthClientRepository: OAuthClientRepository,
private readonly billingService: BillingService,
private readonly config: ConfigService,
private readonly apiKeyRepository: ApiKeysRepository
private readonly apiKeyRepository: ApiKeysRepository,
private readonly platformBookingsService: PlatformBookingsService
) {}
@Get("/")
@@ -166,7 +168,8 @@ export class BookingsController_2024_04_15 {
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<Partial<BookingResponse>>> {
const oAuthClientId = clientId?.toString();
const oAuthClientId =
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
const { orgSlug, locationUrl } = body;
req.headers["x-cal-force-slug"] = orgSlug;
try {
@@ -194,7 +197,7 @@ export class BookingsController_2024_04_15 {
async cancelBooking(
@Req() req: BookingRequest,
@Param("bookingUid") bookingUid: string,
@Body() _: CancelBookingInput_2024_04_15,
@Body() body: CancelBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
@@ -257,11 +260,12 @@ export class BookingsController_2024_04_15 {
@Post("/recurring")
async createRecurringBooking(
@Req() req: BookingRequest,
@Body() _: CreateRecurringBookingInput_2024_04_15[],
@Body() body: CreateRecurringBookingInput_2024_04_15[],
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<BookingResponse[]>> {
const oAuthClientId = clientId?.toString();
const oAuthClientId =
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body[0]?.eventTypeId));
try {
const recurringEventId = uuidv4();
for (const recurringEvent of req.body) {
@@ -296,11 +300,12 @@ export class BookingsController_2024_04_15 {
@Post("/instant")
async createInstantBooking(
@Req() req: BookingRequest,
@Body() _: CreateBookingInput_2024_04_15,
@Body() body: CreateBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
const oAuthClientId = clientId?.toString();
const oAuthClientId =
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
req.userId = (await this.getOwnerId(req)) ?? -1;
try {
const instantMeeting = await handleInstantMeeting(
@@ -346,6 +351,17 @@ export class BookingsController_2024_04_15 {
}
}
private async getOAuthClientIdFromEventType(eventTypeId: number): Promise<string | undefined> {
if (!eventTypeId) {
return undefined;
}
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(eventTypeId);
if (!oAuthClientParams) {
return undefined;
}
return oAuthClientParams.platformClientId;
}
private async getOAuthClientsParams(clientId: string, isEmbed = false): Promise<OAuthRequestParams> {
const res = { ...DEFAULT_PLATFORM_PARAMS };
@@ -396,9 +412,27 @@ export class BookingsController_2024_04_15 {
noEmail: !oAuthParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
if (oAuthClientId) {
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
}
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
async setPlatformAttendeesEmails(requestBody: any, oAuthClientId: string): Promise<void> {
if (requestBody?.responses?.email) {
requestBody.responses.email = await this.platformBookingsService.getPlatformAttendeeEmail(
requestBody.responses.email,
oAuthClientId
);
}
if (requestBody?.responses?.guests && requestBody?.responses?.guests.length) {
requestBody.responses.guests = await this.platformBookingsService.getPlatformAttendeesEmails(
requestBody.responses.guests,
oAuthClientId
);
}
}
private async createNextApiRecurringBookingRequest(
req: BookingRequest,
oAuthClientId?: string,
@@ -425,6 +459,9 @@ export class BookingsController_2024_04_15 {
noEmail: !oAuthParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
});
if (oAuthClientId) {
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
}
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
@@ -0,0 +1,461 @@
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 { 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 { 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 } from "@calcom/platform-constants";
import {
ApiSuccessResponse,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
const CLIENT_REDIRECT_URI = "http://localhost:4321";
describe("Managed user bookings 2024-04-15", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
const platformAdminEmail = `managed-users-bookings-admin-${randomString()}@api.com`;
let platformAdmin: User;
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`;
const thirdManagedUserEmail = `managed-user-bookings-2024-04-15-third-user@api.com`;
let firstManagedUser: CreateManagedUserData;
let secondManagedUser: CreateManagedUserData;
let thirdManagedUser: CreateManagedUserData;
let firstManagedUserEventTypeId: number;
let firstManagedUserBookingsCount = 0;
let secondManagedUserBookingsCount = 0;
let thirdManagedUserBookingsCount = 0;
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);
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
organization = await teamRepositoryFixture.create({
name: `oauth-client-users-organization-${randomString()}`,
isPlatform: true,
isOrganization: true,
});
oAuthClient = await createOAuthClient(organization.id);
await profilesRepositoryFixture.create({
uid: "asd1qwwqeqw-asddsadasd",
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,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
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(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
firstManagedUser = responseBody.data;
});
it("should create an event type for managed user", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Managed user bookings first managed user event type",
slug: "managed-user-bookings-first-managed-user-event-type",
description: "Managed user bookings first managed user event type description",
lengthInMinutes: 30,
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
expect(createdEventType.description).toEqual(body.description);
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
expect(createdEventType.ownerId).toEqual(firstManagedUser.user.id);
firstManagedUserEventTypeId = createdEventType.id;
});
});
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(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
secondManagedUser = responseBody.data;
});
it(`should create third managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: thirdManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Charlie 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(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
thirdManagedUser = responseBody.data;
});
describe("bookings using original emails", () => {
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-21T09:30:00.000Z",
end: "2040-05-21T10:00:00.000Z",
eventTypeId: firstManagedUserEventTypeId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: secondManagedUser.user.name || "unknown-name",
email: secondManagedUserEmail,
location: {
value: "attendeeInPerson",
optionValue: "rome",
},
notes: "test",
guests: [],
},
};
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getFirstManagedUserBookings.body;
const firstManagedUserBookings: GetBookingsDataEntry[] =
firstManagedUserBookingsResponseBody.data.bookings;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getSecondManagedUserBookings.body;
const secondManagedUserBookings: GetBookingsDataEntry[] =
secondManagedUserBookingsResponseBody.data.bookings;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
});
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-21T10:30:00.000Z",
end: "2040-05-21T11:00:00.000Z",
eventTypeId: firstManagedUserEventTypeId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: thirdManagedUser.user.name || "unknown-name",
email: thirdManagedUserEmail,
location: {
value: "attendeeInPerson",
optionValue: "rome",
},
notes: "test",
guests: [secondManagedUserEmail],
},
};
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
thirdManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getFirstManagedUserBookings.body;
const firstManagedUserBookings: GetBookingsDataEntry[] =
firstManagedUserBookingsResponseBody.data.bookings;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getSecondManagedUserBookings.body;
const secondManagedUserBookings: GetBookingsDataEntry[] =
secondManagedUserBookingsResponseBody.data.bookings;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
const getThirdManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getThirdManagedUserBookings.body;
const thirdManagedUserBookings: GetBookingsDataEntry[] =
thirdManagedUserBookingsResponseBody.data.bookings;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
});
describe("bookings using OAuth client emails", () => {
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-21T12:00:00.000Z",
end: "2040-05-21T12:30:00.000Z",
eventTypeId: firstManagedUserEventTypeId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: secondManagedUser.user.name || "unknown-name",
email: secondManagedUser.user.email,
location: {
value: "attendeeInPerson",
optionValue: "rome",
},
notes: "test",
guests: [],
},
};
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getFirstManagedUserBookings.body;
const firstManagedUserBookings: GetBookingsDataEntry[] =
firstManagedUserBookingsResponseBody.data.bookings;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getSecondManagedUserBookings.body;
const secondManagedUserBookings: GetBookingsDataEntry[] =
secondManagedUserBookingsResponseBody.data.bookings;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
});
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-21T13:00:00.000Z",
end: "2040-05-21T13:30:00.000Z",
eventTypeId: firstManagedUserEventTypeId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: thirdManagedUser.user.name || "unknown-name",
email: thirdManagedUser.user.email,
location: {
value: "attendeeInPerson",
optionValue: "rome",
},
notes: "test",
guests: [secondManagedUser.user.email],
},
};
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
thirdManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getFirstManagedUserBookings.body;
const firstManagedUserBookings: GetBookingsDataEntry[] =
firstManagedUserBookingsResponseBody.data.bookings;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getSecondManagedUserBookings.body;
const secondManagedUserBookings: GetBookingsDataEntry[] =
secondManagedUserBookingsResponseBody.data.bookings;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
const getThirdManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getThirdManagedUserBookings.body;
const thirdManagedUserBookings: GetBookingsDataEntry[] =
thirdManagedUserBookingsResponseBody.data.bookings;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
});
function getOAuthUserEmail(oAuthClientId: string, userEmail: string) {
const [username, emailDomain] = userEmail.split("@");
return `${username}+${oAuthClientId}@${emailDomain}`;
}
afterAll(async () => {
await userRepositoryFixture.delete(firstManagedUser.user.id);
await userRepositoryFixture.delete(secondManagedUser.user.id);
await userRepositoryFixture.delete(thirdManagedUser.user.id);
await userRepositoryFixture.delete(platformAdmin.id);
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await app.close();
});
});
@@ -8,8 +8,9 @@ import {
IsOptional,
IsArray,
IsObject,
IsEmail,
ValidateNested,
isEmail,
Validate,
} from "class-validator";
class Location {
@@ -27,14 +28,17 @@ class Response {
@ApiProperty()
name!: string;
@IsEmail()
@Validate((value: string) => !value || isEmail(value), {
message: "Invalid response email",
})
@ApiProperty()
email!: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: [String] })
guests!: string[];
@ApiPropertyOptional({ type: [String] })
guests?: string[];
@IsOptional()
@ValidateNested()
@@ -113,8 +117,9 @@ export class CreateBookingInput_2024_04_15 {
@ApiPropertyOptional()
seatReferenceUid?: string;
@Type(() => Response)
@ApiProperty({ type: Response })
@ValidateNested()
@Type(() => Response)
responses!: Response;
@IsString()
@@ -160,7 +160,7 @@ class User {
email!: string;
}
class GetBookingsDataEntry {
export class GetBookingsDataEntry {
@IsInt()
@ApiProperty()
id!: number;
@@ -3,12 +3,17 @@ import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controll
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { BillingModule } from "@/modules/billing/billing.module";
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
@@ -18,11 +23,22 @@ import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, RedisModule, TokensModule, BillingModule, UsersModule, BookingSeatModule],
imports: [
PrismaModule,
RedisModule,
TokensModule,
BillingModule,
UsersModule,
BookingSeatModule,
SchedulesModule_2024_04_15,
EventTypesModule_2024_04_15,
EventTypesModule_2024_06_14,
],
providers: [
TokensRepository,
OAuthFlowService,
OAuthClientRepository,
OAuthClientUsersService,
BookingsService_2024_08_13,
InputBookingsService_2024_08_13,
OutputBookingsService_2024_08_13,
@@ -30,6 +46,7 @@ import { Module } from "@nestjs/common";
EventTypesRepository_2024_06_14,
BookingSeatRepository,
ApiKeysRepository,
PlatformBookingsService,
],
controllers: [BookingsController_2024_08_13],
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
@@ -189,7 +189,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
createdBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -238,7 +238,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
rescheduledBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -451,7 +451,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.bookingFieldsResponses.name).toEqual(splitName);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -476,7 +476,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0].bookingFieldsResponses.name).toEqual(splitName);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -536,7 +536,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.bookingFieldsResponses["video-url"]).toEqual("http://video-url.com");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -174,7 +174,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(dbBooking?.status).toEqual("ACCEPTED");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -235,7 +235,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(dbBooking?.status).toEqual("REJECTED");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -265,7 +265,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -322,7 +322,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -385,7 +385,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -442,7 +442,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -570,7 +570,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
emailsDisabledSetup.collectiveEventType.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -610,7 +610,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
emailsDisabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -787,7 +787,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
emailsEnabledSetup.collectiveEventType.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -827,7 +827,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
emailsEnabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -261,7 +261,7 @@ describe("Bookings Endpoints 2024-08-13 user emails", () => {
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -381,7 +381,7 @@ describe("Bookings Endpoints 2024-08-13 user emails", () => {
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -0,0 +1,487 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
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 { 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 { 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 { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.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,
VERSION_2024_08_13,
} from "@calcom/platform-constants";
import {
ApiSuccessResponse,
BookingOutput_2024_08_13,
CreateBookingInput_2024_08_13,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
GetBookingsOutput_2024_08_13,
} from "@calcom/platform-types";
const CLIENT_REDIRECT_URI = "http://localhost:4321";
describe("Managed user bookings 2024-08-13", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
let platformAdmin: User;
const managedUsersTimeZone = "Europe/Rome";
const firstManagedUserEmail = `managed-user-bookings-2024-08-13-first-user-${randomString()}@api.com`;
const secondManagedUserEmail = `managed-user-bookings-2024-08-13-second-user-${randomString()}@api.com`;
const thirdManagedUserEmail = `managed-user-bookings-2024-08-13-third-user-${randomString()}@api.com`;
let firstManagedUser: CreateManagedUserData;
let secondManagedUser: CreateManagedUserData;
let thirdManagedUser: CreateManagedUserData;
let firstManagedUserEventTypeId: number;
let firstManagedUserBookingsCount = 0;
let secondManagedUserBookingsCount = 0;
let thirdManagedUserBookingsCount = 0;
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);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
organization = await teamRepositoryFixture.create({
name: `oauth-client-users-organization-${randomString()}`,
isPlatform: true,
isOrganization: true,
});
oAuthClient = await createOAuthClient(organization.id);
await profilesRepositoryFixture.create({
uid: "asd1qwwqeqw-asddsadasd",
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,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
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(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
firstManagedUser = responseBody.data;
});
it("should create an event type for managed user", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Managed user bookings first managed user event type",
slug: "managed-user-bookings-first-managed-user-event-type",
description: "Managed user bookings first managed user event type description",
lengthInMinutes: 30,
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
expect(createdEventType.description).toEqual(body.description);
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
expect(createdEventType.ownerId).toEqual(firstManagedUser.user.id);
firstManagedUserEventTypeId = createdEventType.id;
});
});
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(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
secondManagedUser = responseBody.data;
});
it(`should create third managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: thirdManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Charlie 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(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
thirdManagedUser = responseBody.data;
});
describe("bookings using original emails", () => {
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: secondManagedUser.user.name!,
email: secondManagedUserEmail,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
location: "https://meet.google.com/abc-def-ghi",
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
});
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: thirdManagedUser.user.name!,
email: thirdManagedUserEmail,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
guests: [secondManagedUserEmail],
location: "https://meet.google.com/abc-def-ghi",
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
thirdManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
const getThirdManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getThirdManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
});
describe("bookings using OAuth client emails", () => {
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: secondManagedUser.user.name!,
email: secondManagedUser.user.email,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
location: "https://meet.google.com/abc-def-ghi",
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
});
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 15, 30, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: thirdManagedUser.user.name!,
email: thirdManagedUser.user.email,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
guests: [secondManagedUser.user.email],
location: "https://meet.google.com/abc-def-ghi",
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
thirdManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
const getThirdManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getThirdManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
});
function getOAuthUserEmail(oAuthClientId: string, userEmail: string) {
const [username, emailDomain] = userEmail.split("@");
return `${username}+${oAuthClientId}@${emailDomain}`;
}
afterAll(async () => {
await userRepositoryFixture.delete(firstManagedUser.user.id);
await userRepositoryFixture.delete(secondManagedUser.user.id);
await userRepositoryFixture.delete(thirdManagedUser.user.id);
await userRepositoryFixture.delete(platformAdmin.id);
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await app.close();
});
});
@@ -684,7 +684,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should cancel all remaning recurrences", async () => {
it("should cancel all remaining recurrences", async () => {
const recurringBookingUid = recurringBooking[0].recurringBookingUid;
return request(app.getHttpServer())
@@ -401,7 +401,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -455,7 +455,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -515,7 +515,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -407,7 +407,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
createdBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -547,7 +547,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.createdAt).toEqual(createdBooking.createdAt);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -607,7 +607,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -1480,7 +1480,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.cancelledByEmail).toEqual(cancelledByEmail);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -1530,7 +1530,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.rescheduledByEmail).toEqual(rescheduledByEmail);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -194,7 +194,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.duration).toEqual(variableLengthEventType.length);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -239,7 +239,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.duration).toEqual(lengthInMinutes);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -1,6 +1,7 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { BillingService } from "@/modules/billing/services/billing.service";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
@@ -67,7 +68,8 @@ export class BookingsService_2024_08_13 {
private readonly prismaReadService: PrismaReadService,
private readonly billingService: BillingService,
private readonly usersService: UsersService,
private readonly usersRepository: UsersRepository
private readonly usersRepository: UsersRepository,
private readonly platformBookingsService: PlatformBookingsService
) {}
async createBooking(request: Request, body: CreateBookingInput) {
@@ -368,7 +370,7 @@ export class BookingsService_2024_08_13 {
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
const platformClientParams = bookingBefore?.eventTypeId
? await this.inputService.getOAuthClientParams(bookingBefore.eventTypeId)
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
: undefined;
await handleMarkNoShow({
@@ -432,7 +434,7 @@ export class BookingsService_2024_08_13 {
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
@@ -471,7 +473,7 @@ export class BookingsService_2024_08_13 {
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
@@ -498,7 +500,7 @@ export class BookingsService_2024_08_13 {
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
@@ -526,7 +528,7 @@ export class BookingsService_2024_08_13 {
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
@@ -3,11 +3,11 @@ import {
bookingResponsesSchema,
seatedBookingDataSchema,
} from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { Logger } from "@nestjs/common";
@@ -19,7 +19,7 @@ import { NextApiRequest } from "next/types";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries";
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
import {
CancelBookingInput,
CancelBookingInput_2024_08_13,
@@ -33,7 +33,7 @@ import {
RescheduleBookingInput_2024_08_13,
RescheduleSeatedBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { EventType, PlatformOAuthClient } from "@calcom/prisma/client";
import { EventType } from "@calcom/prisma/client";
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
@@ -70,20 +70,20 @@ export class InputBookingsService_2024_08_13 {
constructor(
private readonly oAuthFlowService: OAuthFlowService,
private readonly oAuthClientRepository: OAuthClientRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly bookingsRepository: BookingsRepository_2024_08_13,
private readonly config: ConfigService,
private readonly apiKeyRepository: ApiKeysRepository,
private readonly bookingSeatRepository: BookingSeatRepository
private readonly bookingSeatRepository: BookingSeatRepository,
private readonly platformBookingsService: PlatformBookingsService
) {}
async createBookingRequest(
request: Request,
body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13
): Promise<BookingRequest> {
const bodyTransformed = await this.transformInputCreateBooking(body);
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(body.eventTypeId);
const bodyTransformed = await this.transformInputCreateBooking(body, oAuthClientParams?.platformClientId);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
@@ -111,30 +111,7 @@ export class InputBookingsService_2024_08_13 {
return newRequest as unknown as BookingRequest;
}
async getOAuthClientParams(eventTypeId: number) {
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
let oAuthClient: PlatformOAuthClient | null = null;
if (eventType?.userId) {
oAuthClient = await this.oAuthClientRepository.getByUserId(eventType.userId);
} else if (eventType?.teamId) {
oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId);
}
if (oAuthClient) {
return {
platformClientId: oAuthClient.id,
platformCancelUrl: oAuthClient.bookingCancelRedirectUri,
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
platformBookingUrl: oAuthClient.bookingRedirectUri,
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
};
}
return undefined;
}
async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) {
async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13, platformClientId?: string) {
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
inputBooking.eventTypeId
);
@@ -151,7 +128,17 @@ export class InputBookingsService_2024_08_13 {
);
const endTime = startTime.plus({ minutes: lengthInMinutes });
const guests = inputBooking.guests;
const guests =
inputBooking.guests && platformClientId
? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId)
: inputBooking.guests;
const attendeeEmail =
inputBooking.attendee.email && platformClientId
? await this.platformBookingsService.getPlatformAttendeeEmail(
inputBooking.attendee.email,
platformClientId
)
: inputBooking.attendee.email;
return {
start: startTime.toISO(),
@@ -163,20 +150,13 @@ export class InputBookingsService_2024_08_13 {
hasHashedBookingLink: false,
guests,
// note(Lauris): responses with name and email are required by the handleNewBooking
responses: inputBooking.bookingFieldsResponses
? {
...inputBooking.bookingFieldsResponses,
name: inputBooking.attendee.name,
email: inputBooking.attendee.email ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
}
: {
name: inputBooking.attendee.name,
email: inputBooking.attendee.email ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
},
responses: {
...(inputBooking.bookingFieldsResponses || {}),
name: inputBooking.attendee.name,
email: attendeeEmail ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
},
};
}
@@ -203,9 +183,12 @@ export class InputBookingsService_2024_08_13 {
request: Request,
body: CreateRecurringBookingInput_2024_08_13
): Promise<BookingRequest> {
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(body.eventTypeId);
// note(Lauris): update to this.transformInputCreate when rescheduling is implemented
const bodyTransformed = await this.transformInputCreateRecurringBooking(body);
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
const bodyTransformed = await this.transformInputCreateRecurringBooking(
body,
oAuthClientParams?.platformClientId
);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
@@ -231,7 +214,10 @@ export class InputBookingsService_2024_08_13 {
return newRequest as unknown as BookingRequest;
}
async transformInputCreateRecurringBooking(inputBooking: CreateRecurringBookingInput_2024_08_13) {
async transformInputCreateRecurringBooking(
inputBooking: CreateRecurringBookingInput_2024_08_13,
platformClientId?: string
) {
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
inputBooking.eventTypeId
);
@@ -261,7 +247,17 @@ export class InputBookingsService_2024_08_13 {
inputBooking.attendee.timeZone
);
const guests = inputBooking.guests;
const guests =
inputBooking.guests && platformClientId
? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId)
: inputBooking.guests;
const attendeeEmail =
inputBooking.attendee.email && platformClientId
? await this.platformBookingsService.getPlatformAttendeeEmail(
inputBooking.attendee.email,
platformClientId
)
: inputBooking.attendee.email;
for (let i = 0; i < repeatsTimes; i++) {
const endTime = startTime.plus({ minutes: eventType.length });
@@ -277,14 +273,12 @@ export class InputBookingsService_2024_08_13 {
hasHashedBookingLink: false,
guests,
// note(Lauris): responses with name and email are required by the handleNewBooking
responses: inputBooking.bookingFieldsResponses
? {
...inputBooking.bookingFieldsResponses,
name: inputBooking.attendee.name,
email: inputBooking.attendee.email,
guests,
}
: { name: inputBooking.attendee.name, email: inputBooking.attendee.email, guests },
responses: {
...(inputBooking.bookingFieldsResponses || {}),
name: inputBooking.attendee.name,
email: attendeeEmail,
guests,
},
schedulingType: eventType.schedulingType,
});
@@ -315,7 +309,9 @@ export class InputBookingsService_2024_08_13 {
? await this.transformInputRescheduleSeatedBooking(bookingUid, body)
: await this.transformInputRescheduleBooking(bookingUid, body);
const oAuthClientParams = await this.getOAuthClientParams(bodyTransformed.eventTypeId);
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(
bodyTransformed.eventTypeId
);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
@@ -446,7 +442,8 @@ export class InputBookingsService_2024_08_13 {
return keyData?.userId;
} else {
// Access Token
return this.oAuthFlowService.getOwnerId(bearerToken);
const ownerId = await this.oAuthFlowService.getOwnerId(bearerToken);
return ownerId;
}
}
} catch (err) {
@@ -501,7 +498,7 @@ export class InputBookingsService_2024_08_13 {
}
const oAuthClientParams = booking.eventTypeId
? await this.getOAuthClientParams(booking.eventTypeId)
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const newRequest = { ...request };
@@ -0,0 +1,59 @@
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { PlatformOAuthClient } from "@calcom/prisma/client";
@Injectable()
export class PlatformBookingsService {
constructor(
private readonly oAuthClientsUsersService: OAuthClientUsersService,
private readonly usersRepository: UsersRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly oAuthClientRepository: OAuthClientRepository
) {}
async getPlatformAttendeesEmails(guestsEmails: string[], platformClientId: string) {
return Promise.all(
guestsEmails.map((guestEmail) => this.getPlatformAttendeeEmail(guestEmail, platformClientId))
);
}
async getPlatformAttendeeEmail(attendeeEmail: string, platformClientId: string) {
if (!attendeeEmail.includes(platformClientId)) {
// note(Lauris): we need to do this when managed user is booking another managed user and the managed user doing the booking entered their email without oAuth client id
// or if one of the guests added are managed users without oAuth client id in their email.
const oAuthUserEmail = this.oAuthClientsUsersService.getOAuthUserEmail(platformClientId, attendeeEmail);
const oAuthUser = await this.usersRepository.findByEmail(oAuthUserEmail);
if (oAuthUser) {
return oAuthUserEmail;
}
}
return attendeeEmail;
}
async getOAuthClientParams(eventTypeId: number) {
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
let oAuthClient: PlatformOAuthClient | null = null;
if (eventType?.userId) {
oAuthClient = await this.oAuthClientRepository.getByUserId(eventType.userId);
} else if (eventType?.teamId) {
oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId);
}
if (oAuthClient) {
return {
platformClientId: oAuthClient.id,
platformCancelUrl: oAuthClient.bookingCancelRedirectUri,
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
platformBookingUrl: oAuthClient.bookingRedirectUri,
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
};
}
return undefined;
}
}
@@ -131,7 +131,7 @@ class Primary {
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
domainWideDelegationCredentialId?: string | null;
delegationCredentialId?: string | null;
}
export class Calendar {
@@ -174,7 +174,7 @@ export class Calendar {
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
domainWideDelegationCredentialId?: string | null;
delegationCredentialId?: string | null;
}
export class ConnectedCalendar {
@@ -190,7 +190,7 @@ export class ConnectedCalendar {
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
domainWideDelegationCredentialId?: string | null;
delegationCredentialId?: string | null;
@ValidateNested()
@IsObject()
@@ -208,7 +208,7 @@ export class ConnectedCalendar {
class DestinationCalendar {
@IsInt()
@ApiProperty()
id!: number;
id!: number | string;
@IsString()
@ApiProperty()
@@ -238,7 +238,7 @@ class DestinationCalendar {
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
domainWideDelegationCredentialId?: string | null;
delegationCredentialId?: string | null;
@IsString()
@IsOptional()
@@ -39,7 +39,7 @@ export class CalendarsService {
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
) {}
private buildNonDwdCredentials<TCredential>(credentials: TCredential[]) {
private buildNonDelegationCredentials<TCredential>(credentials: TCredential[]) {
return credentials
.map((credential) => ({
...credential,
@@ -83,11 +83,12 @@ export class CalendarsService {
);
try {
const calendarBusyTimes = await getBusyCalendarTimes(
this.buildNonDwdCredentials(credentials),
this.buildNonDelegationCredentials(credentials),
dateFrom,
dateTo,
composedSelectedCalendars
);
// @ts-expect-error Element implicitly has any type
const calendarBusyTimesConverted = calendarBusyTimes.map((busyTime) => {
const busyTimeStart = DateTime.fromJSDate(new Date(busyTime.start)).setZone(timezone);
const busyTimeEnd = DateTime.fromJSDate(new Date(busyTime.end)).setZone(timezone);
@@ -33,10 +33,10 @@ import {
} from "@calcom/platform-constants";
import {
EventTypesByViewer,
EventTypesPublic,
eventTypeBookingFields,
eventTypeLocations,
} from "@calcom/platform-libraries";
EventTypesPublic,
} from "@calcom/platform-libraries/event-types";
import { ApiSuccessResponse } from "@calcom/platform-types";
describe("Event types Endpoints", () => {
@@ -5,7 +5,7 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { getEventTypeById } from "@calcom/platform-libraries";
import { getEventTypeById } from "@calcom/platform-libraries/event-types";
import type { PrismaClient } from "@calcom/prisma";
@Injectable()
@@ -1,6 +1,8 @@
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_04_15/constants/constants";
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/editable";
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
@@ -16,7 +18,7 @@ import {
EventTypesPublic,
getEventTypesPublic,
systemBeforeFieldEmail,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/event-types";
import { EventType } from "@calcom/prisma/client";
@Injectable()
@@ -68,6 +70,7 @@ export class EventTypesService_2024_04_15 {
organization: { isOrgAdmin },
profile: { id: profileId },
metadata: user.metadata,
email: user.email,
};
}
@@ -134,7 +137,7 @@ export class EventTypesService_2024_04_15 {
!bookingFields.find((field) => field.type === "email") &&
!bookingFields.find((field) => field.type === "phone")
) {
bookingFields.push(systemBeforeFieldEmail);
bookingFields.push({ ...systemBeforeFieldEmail, type: BaseField.email, editable: Editable.system });
}
await updateEventType({
@@ -167,7 +170,15 @@ export class EventTypesService_2024_04_15 {
async getUserToUpdateEvent(user: UserWithProfile) {
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
return { ...user, profile: { id: profileId }, selectedCalendars };
const eventTypeSelectedCalendars =
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
return {
...user,
locale: user.locale ?? "en",
profile: { id: profileId },
userLevelSelectedCalendars: selectedCalendars,
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
};
}
async deleteEventType(eventTypeId: number, userId: number) {
@@ -1845,7 +1845,6 @@ describe("Event types Endpoints", () => {
await teamRepositoryFixture.delete(organization.id);
try {
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
} catch (e) {
// Event type might have been deleted by the test
}
@@ -8,9 +8,13 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { createEventType, updateEventType } from "@calcom/platform-libraries";
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
import { dynamicEvent } from "@calcom/platform-libraries";
import {
createEventType,
updateEventType,
getEventTypesPublic,
EventTypesPublic,
} from "@calcom/platform-libraries/event-types";
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
import { EventType } from "@calcom/prisma/client";
@@ -112,8 +116,11 @@ export class EventTypesService_2024_06_14 {
: false;
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
const eventTypeSelectedCalendars =
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
return {
id: user.id,
locale: user.locale ?? "en",
role: user.role,
username: user.username,
organizationId: user.organizationId,
@@ -121,6 +128,9 @@ export class EventTypesService_2024_06_14 {
profile: { id: profileId },
metadata: user.metadata,
selectedCalendars,
email: user.email,
userLevelSelectedCalendars: selectedCalendars,
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
};
}
@@ -204,7 +214,11 @@ export class EventTypesService_2024_06_14 {
return defaultEventTypes;
}
async updateEventType(eventTypeId: number, body: InputEventTransformed_2024_06_14, user: UserWithProfile) {
async updateEventType(
eventTypeId: number,
body: Partial<InputEventTransformed_2024_06_14>,
user: UserWithProfile
) {
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
@@ -230,7 +244,7 @@ export class EventTypesService_2024_06_14 {
};
}
async checkCanUpdateEventType(userId: number, eventTypeId: number, scheduleId: number | undefined) {
async checkCanUpdateEventType(userId: number, eventTypeId: number, scheduleId: number | undefined | null) {
const existingEventType = await this.getUserEventType(userId, eventTypeId);
if (!existingEventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
@@ -242,7 +256,15 @@ export class EventTypesService_2024_06_14 {
async getUserToUpdateEvent(user: UserWithProfile) {
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
return { ...user, profile: { id: profileId }, selectedCalendars };
const eventTypeSelectedCalendars =
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
return {
...user,
locale: user.locale ?? "en",
profile: { id: profileId },
userLevelSelectedCalendars: selectedCalendars,
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
};
}
async deleteEventType(eventTypeId: number, userId: number) {
@@ -25,13 +25,17 @@ import {
transformSeatsApiToInternal,
SystemField,
CustomField,
} from "@calcom/platform-libraries";
InternalLocation,
InternalLocationSchema,
} from "@calcom/platform-libraries/event-types";
import {
CreateEventTypeInput_2024_06_14,
DestinationCalendar_2024_06_14,
InputEventTransformed_2024_06_14,
OutputUnknownLocation_2024_06_14,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
@@ -58,7 +62,7 @@ export class InputEventTypesService_2024_06_14 {
const transformedBody = this.transformInputCreateEventType(inputEventType);
await this.validateEventTypeInputs({
seatsPerTimeSlot: transformedBody.seatsPerTimeSlot,
seatsPerTimeSlot: transformedBody?.seatsPerTimeSlot || null,
locations: transformedBody.locations,
requiresConfirmation: transformedBody.requiresConfirmation,
eventName: transformedBody.eventName,
@@ -272,7 +276,12 @@ export class InputEventTypesService_2024_06_14 {
}
transformInputBookerLayouts(inputBookerLayouts: CreateEventTypeInput_2024_06_14["bookerLayouts"]) {
return transformBookerLayoutsApiToInternal(inputBookerLayouts);
const layouts = transformBookerLayoutsApiToInternal(inputBookerLayouts);
if (!layouts) return undefined;
return {
defaultLayout: layouts.defaultLayout as unknown as BookerLayouts,
enabledLayouts: layouts.enabledLayouts as unknown as BookerLayouts[],
};
}
transformInputConfirmationPolicy(
@@ -303,23 +312,22 @@ export class InputEventTypesService_2024_06_14 {
eventName,
}: ValidationContext) {
let seatsPerTimeSlotDb: number | null = null;
let locationsDb: ReturnType<typeof this.transformInputLocations> = [];
let locationsDb: ReturnType<typeof this.transformLocations> = [];
let requiresConfirmationDb = false;
if (eventTypeId != null) {
const eventTypeDb = await this.eventTypesRepository.getEventTypeWithSeats(eventTypeId);
seatsPerTimeSlotDb = eventTypeDb?.seatsPerTimeSlot ?? null;
locationsDb = this.outputEventTypesService.transformLocations(eventTypeDb?.locations) ?? [];
locationsDb = this.transformLocations(eventTypeDb?.locations) ?? [];
requiresConfirmationDb = eventTypeDb?.requiresConfirmation ?? false;
}
const seatsPerTimeSlotFinal = seatsPerTimeSlot !== undefined ? seatsPerTimeSlot : seatsPerTimeSlotDb;
const seatsEnabledFinal = seatsPerTimeSlotFinal != null && seatsPerTimeSlotFinal > 0;
const seatsPerTimeSlotFinal = !!seatsPerTimeSlot ? seatsPerTimeSlot : seatsPerTimeSlotDb;
const seatsEnabledFinal = !!seatsPerTimeSlotFinal && seatsPerTimeSlotFinal > 0;
const locationsFinal = locations !== undefined ? locations : locationsDb;
const requiresConfirmationFinal =
requiresConfirmation !== undefined ? requiresConfirmation : requiresConfirmationDb;
this.validateSeatsSingleLocationRule(seatsEnabledFinal, locationsFinal);
this.validateSeatsRequiresConfirmationFalseRule(seatsEnabledFinal, requiresConfirmationFinal);
this.validateMultipleLocationsSeatsDisabledRule(locationsFinal, seatsEnabledFinal);
@@ -331,7 +339,7 @@ export class InputEventTypesService_2024_06_14 {
}
validateSeatsSingleLocationRule(
seatsEnabled: boolean,
locations: ReturnType<typeof this.transformInputLocations>
locations: ReturnType<typeof this.transformLocations>
) {
if (seatsEnabled && locations.length > 1) {
throw new BadRequestException(
@@ -339,6 +347,22 @@ export class InputEventTypesService_2024_06_14 {
);
}
}
transformLocations(locations: any) {
if (!locations) return [];
const knownLocations: InternalLocation[] = [];
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
for (const location of locations) {
const result = InternalLocationSchema.safeParse(location);
if (result.success) {
knownLocations.push(result.data);
} else {
unknownLocations.push({ type: "unknown", location: JSON.stringify(location) });
}
}
return [...knownLocations];
}
validateSeatsRequiresConfirmationFalseRule(seatsEnabled: boolean, requiresConfirmation: boolean) {
if (seatsEnabled && requiresConfirmation) {
@@ -349,7 +373,7 @@ export class InputEventTypesService_2024_06_14 {
}
validateMultipleLocationsSeatsDisabledRule(
locations: ReturnType<typeof this.transformInputLocations>,
locations: ReturnType<typeof this.transformLocations>,
seatsEnabled: boolean
) {
if (locations.length > 1 && seatsEnabled) {
@@ -2,15 +2,18 @@ import { Injectable } from "@nestjs/common";
import type { EventType, User, Schedule, DestinationCalendar } from "@prisma/client";
import {
EventTypeMetaDataSchema,
userMetadata,
parseBookingLimit,
parseRecurringEvent,
getBookingFieldsWithSystemFields,
} from "@calcom/platform-libraries";
import {
EventTypeMetaDataSchema,
transformLocationsInternalToApi,
transformBookingFieldsInternalToApi,
parseRecurringEvent,
InternalLocationSchema,
SystemField,
CustomField,
parseBookingLimit,
transformIntervalLimitsInternalToApi,
transformFutureBookingLimitsInternalToApi,
transformRecurrenceInternalToApi,
@@ -21,8 +24,7 @@ import {
transformSeatsInternalToApi,
InternalLocation,
BookingFieldSchema,
getBookingFieldsWithSystemFields,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/event-types";
import {
TransformFutureBookingsLimitSchema_2024_06_14,
BookerLayoutsTransformedSchema,
@@ -198,13 +200,13 @@ export class OutputEventTypesService_2024_06_14 {
};
}
transformLocations(locations: any) {
if (!locations) return [];
transformLocations(locationDb: any) {
if (!locationDb) return [];
const knownLocations: InternalLocation[] = [];
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
for (const location of locations) {
for (const location of locationDb) {
const result = InternalLocationSchema.safeParse(location);
if (result.success) {
knownLocations.push(result.data);
@@ -315,6 +317,7 @@ export class OutputEventTypesService_2024_06_14 {
transformEventTypeColor(eventTypeColor: any) {
if (!eventTypeColor) return undefined;
const parsedeventTypeColor = parseEventTypeColor(eventTypeColor);
if (!parsedeventTypeColor) return undefined;
return transformEventTypeColorsInternalToApi(parsedeventTypeColor);
}
@@ -1,3 +1,4 @@
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
import { Request } from "express";
@@ -18,7 +19,7 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
exception,
body: request.body,
headers: request.headers,
headers: filterReqHeaders(request.headers),
url: request.url,
method: request.method,
requestId,
@@ -1,3 +1,4 @@
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
import { Catch, HttpStatus, Logger } from "@nestjs/common";
import {
@@ -44,7 +45,7 @@ export class PrismaExceptionFilter implements ExceptionFilter {
this.logger.error(`PrismaError: ${error.message}`, {
error,
body: request.body,
headers: request.headers,
headers: filterReqHeaders(request.headers),
url: request.url,
method: request.method,
requestId,
@@ -1,3 +1,4 @@
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
import { Request } from "express";
@@ -46,7 +47,7 @@ export class TRPCExceptionFilter implements ExceptionFilter {
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
exception,
body: request.body,
headers: request.headers,
headers: filterReqHeaders(request.headers),
url: request.url,
method: request.method,
requestId,
@@ -1,3 +1,4 @@
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
import { Catch, HttpStatus, Logger } from "@nestjs/common";
import { Request } from "express";
@@ -19,7 +20,7 @@ export class ZodExceptionFilter implements ExceptionFilter {
this.logger.error(`ZodError: ${error.message}`, {
error,
body: request.body,
headers: request.headers,
headers: filterReqHeaders(request.headers),
url: request.url,
method: request.method,
requestId,
+20
View File
@@ -0,0 +1,20 @@
import { IncomingHttpHeaders } from "http";
export function filterReqHeaders(headers: IncomingHttpHeaders): Partial<IncomingHttpHeaders> {
return {
"Content-Type": headers["content-type"],
"User-Agent": headers["user-agent"],
"X-Request-Id": headers["X-Request-Id"] ?? headers["x-request-id"],
// IP-related headers
"X-Forwarded-For": headers["x-forwarded-for"],
"X-Forwarded-Host": headers["x-forwarded-host"],
"CF-Connecting-IP": headers["cf-connecting-ip"],
"CloudFront-Viewer-Address": headers["cloudfront-viewer-address"],
// Vercel headers
"x-vercel-id": headers["x-vercel-id"],
"x-vercel-deployment-url": headers["x-vercel-deployment-url"],
"x-vercel-country": headers["x-vercel-country"],
"x-vercel-region": headers["x-vercel-region"],
Host: headers["host"],
};
}
@@ -1,3 +1,4 @@
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import { Injectable, NestMiddleware, Logger } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
import { v4 as uuid } from "uuid";
@@ -15,7 +16,7 @@ export class RequestIdMiddleware implements NestMiddleware {
requestId,
method,
url: baseUrl,
headers,
headers: filterReqHeaders(headers),
requestBody,
timestamp: new Date().toISOString(),
});
@@ -23,8 +23,8 @@ import {
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries";
import { ConnectedApps } from "@calcom/platform-libraries";
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries/event-types";
import { ApiResponse } from "@calcom/platform-types";
/*
@@ -81,7 +81,7 @@ export class AtomsController {
): Promise<ApiResponse<unknown>> {
const { teamId } = queryParams;
const app = await this.eventTypesService.getEventTypesAppIntegration(appSlug, user.id, user.name, teamId);
const app = await this.eventTypesService.getEventTypesAppIntegration(appSlug, user, teamId);
return {
status: SUCCESS_STATUS,
@@ -124,7 +124,11 @@ export class AtomsController {
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@Body() body: UpdateEventTypeReturn
): Promise<ApiResponse<UpdateEventTypeReturn>> {
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
const eventType = await this.eventTypesService.updateEventType(
eventTypeId,
{ ...body, id: eventTypeId },
user
);
return {
status: SUCCESS_STATUS,
data: eventType,
@@ -140,7 +144,12 @@ export class AtomsController {
@Param("teamId", ParseIntPipe) teamId: number,
@Body() body: UpdateEventTypeReturn
): Promise<ApiResponse<UpdateEventTypeReturn>> {
const eventType = await this.eventTypesService.updateTeamEventType(eventTypeId, body, user, teamId);
const eventType = await this.eventTypesService.updateTeamEventType(
eventTypeId,
{ ...body, id: eventTypeId },
user,
teamId
);
return {
status: SUCCESS_STATUS,
data: eventType,
@@ -10,17 +10,11 @@ import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
import {
updateEventType,
TUpdateEventTypeInputSchema,
systemBeforeFieldEmail,
getEventTypeById,
getEnabledAppsFromCredentials,
getAppFromSlug,
MembershipRole,
EventTypeMetaDataSchema,
getClientSecretFromPayment,
getBulkEventTypes,
bulkUpdateEventsToDefaultLocation,
enrichUserWithDelegationConferencingCredentialsWithoutOrgId,
} from "@calcom/platform-libraries";
import type {
App,
@@ -31,6 +25,17 @@ import type {
TDependencyData,
CredentialPayload,
} from "@calcom/platform-libraries";
import {
getBulkEventTypes,
getEventTypeById,
bulkUpdateEventsToDefaultLocation,
} from "@calcom/platform-libraries/event-types";
import {
updateEventType,
TUpdateEventTypeInputSchema,
systemBeforeFieldEmail,
EventTypeMetaDataSchema,
} from "@calcom/platform-libraries/event-types";
import { PrismaClient } from "@calcom/prisma";
type EnabledAppType = App & {
@@ -93,9 +98,10 @@ export class EventTypesAtomService {
) {
await this.checkCanUpdateTeamEventType(user.id, eventTypeId, teamId, body.scheduleId);
const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user);
const bookingFields = [...(body.bookingFields || [])];
const bookingFields = body.bookingFields ? [...body.bookingFields] : undefined;
if (
bookingFields?.length &&
!bookingFields.find((field) => field.type === "email") &&
!bookingFields.find((field) => field.type === "phone")
) {
@@ -103,7 +109,7 @@ export class EventTypesAtomService {
}
const eventType = await updateEventType({
input: { id: eventTypeId, ...body, bookingFields },
input: { ...body, id: eventTypeId, bookingFields },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -116,7 +122,7 @@ export class EventTypesAtomService {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
return eventType.eventType;
return eventType;
}
async updateEventType(eventTypeId: number, body: TUpdateEventTypeInputSchema, user: UserWithProfile) {
@@ -133,7 +139,7 @@ export class EventTypesAtomService {
}
const eventType = await updateEventType({
input: { id: eventTypeId, ...body, bookingFields },
input: { ...body, id: eventTypeId, bookingFields },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -146,10 +152,15 @@ export class EventTypesAtomService {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
return eventType.eventType;
return eventType;
}
async checkCanUpdateTeamEventType(userId: number, eventTypeId: number, teamId: number, scheduleId: number) {
async checkCanUpdateTeamEventType(
userId: number,
eventTypeId: number,
teamId: number,
scheduleId: number | null | undefined
) {
await this.checkTeamOwnsEventType(userId, eventTypeId, teamId);
await this.teamEventTypeService.validateEventTypeExists(teamId, eventTypeId);
await this.eventTypeService.checkUserOwnsSchedule(userId, scheduleId);
@@ -178,11 +189,11 @@ export class EventTypesAtomService {
}
}
async getEventTypesAppIntegration(slug: string, userId: number, userName: string | null, teamId?: number) {
let credentials = await this.credentialsRepository.getAllUserCredentialsById(userId);
async getEventTypesAppIntegration(slug: string, user: UserWithProfile, teamId?: number) {
let credentials = await this.credentialsRepository.getAllUserCredentialsById(user.id);
let userTeams: TeamQuery[] = [];
if (teamId) {
const teamsQuery = await this.atomsRepository.getUserTeams(userId);
const teamsQuery = await this.atomsRepository.getUserTeams(user.id);
// If a team is a part of an org then include those apps
// Don't want to iterate over these parent teams
const filteredTeams: TeamQuery[] = [];
@@ -210,8 +221,17 @@ export class EventTypesAtomService {
credentials = credentials.concat(teamAppCredentials);
}
}
//TODO: enrich credentials for DWD
const enabledApps = await getEnabledAppsFromCredentials(credentials, {
// We only add delegationCredentials if the request for location options is for a user because DelegationCredential Credential is applicable to Users only.
const { credentials: allCredentials } = await enrichUserWithDelegationConferencingCredentialsWithoutOrgId(
{
user: {
...user,
credentials,
},
}
);
credentials = allCredentials;
const enabledApps = await getEnabledAppsFromCredentials(allCredentials, {
where: { slug },
});
const apps = await Promise.all(
@@ -264,7 +284,7 @@ export class EventTypesAtomService {
});
}
const credentialOwner: CredentialOwner = {
name: userName,
name: user.name,
teamId,
};
return {
@@ -29,12 +29,12 @@ export class DestinationCalendarsController {
@Body() input: DestinationCalendarsInputBodyDto,
@GetUser() user: UserWithProfile
): Promise<DestinationCalendarsOutputResponseDto> {
const { integration, externalId, domainWideDelegationCredentialId } = input;
const { integration, externalId, delegationCredentialId } = input;
const updatedDestinationCalendar = await this.destinationCalendarsService.updateDestinationCalendars(
integration,
externalId,
user.id,
domainWideDelegationCredentialId
delegationCredentialId
);
return {
@@ -12,21 +12,21 @@ export class DestinationCalendarsRepository {
userId: number,
primaryEmail: string | null,
credentialId?: number,
domainWideDelegationCredentialId?: string
delegationCredentialId?: string
) {
return await this.dbWrite.prisma.destinationCalendar.upsert({
update: {
integration,
externalId,
...(credentialId ? { credentialId } : {}),
...(domainWideDelegationCredentialId ? { domainWideDelegationCredentialId } : {}),
...(delegationCredentialId ? { delegationCredentialId } : {}),
primaryEmail,
},
create: {
integration,
externalId,
...(credentialId ? { credentialId } : {}),
...(domainWideDelegationCredentialId ? { domainWideDelegationCredentialId } : {}),
...(delegationCredentialId ? { delegationCredentialId } : {}),
primaryEmail,
userId,
},
@@ -28,7 +28,7 @@ export class DestinationCalendarsInputBodyDto {
@ApiProperty({
example: "https://caldav.icloud.com/26962146906/calendars/1644422A-1945-4438-BBC0-4F0Q23A57R7S/",
description:
"Unique identifier used to represent the specfic calendar, as returned by the /calendars endpoint",
"Unique identifier used to represent the specific calendar, as returned by the /calendars endpoint",
type: "string",
required: true,
})
@@ -37,5 +37,5 @@ export class DestinationCalendarsInputBodyDto {
@IsString()
@IsOptional()
@ApiPropertyOptional()
readonly domainWideDelegationCredentialId?: string;
readonly delegationCredentialId?: string;
}
@@ -14,10 +14,10 @@ export class DestinationCalendarsService {
integration: string,
externalId: string,
userId: number,
domainWideDelegationCredentialId?: string
delegationCredentialId?: string
) {
// note(Lauris): todo remove the log but leaving this now to confirm domainWideDelegationCredentialId is received
console.log("debug: domainWideDelegationCredentialId", domainWideDelegationCredentialId);
// note(Lauris): todo remove the log but leaving this now to confirm delegationCredentialId is received
console.log("debug: delegationCredentialId", delegationCredentialId);
const userCalendars = await this.calendarsService.getCalendars(userId);
const allCalendars: Calendar[] = userCalendars.connectedCalendars
.map((cal: ConnectedCalendar) => cal.calendars ?? [])
@@ -27,25 +27,25 @@ export class DestinationCalendarsService {
cal.externalId === externalId && cal.integration === integration && cal.readOnly === false
)?.credentialId;
if (!domainWideDelegationCredentialId && !credentialId) {
if (!delegationCredentialId && !credentialId) {
throw new NotFoundException(`Could not find calendar ${externalId}`);
}
const dwdCalendar = domainWideDelegationCredentialId
const delegatedCalendar = delegationCredentialId
? allCalendars.find(
(cal: Calendar) =>
cal.externalId === externalId &&
cal.integration === integration &&
cal.domainWideDelegationCredentialId === domainWideDelegationCredentialId
cal.delegationCredentialId === delegationCredentialId
)
: undefined;
if (domainWideDelegationCredentialId && !dwdCalendar) {
if (delegationCredentialId && !delegatedCalendar) {
throw new NotFoundException(`Could not find calendar ${externalId}`);
}
const primaryEmail = dwdCalendar
? (dwdCalendar.primary && dwdCalendar?.email) || undefined
const primaryEmail = delegatedCalendar
? (delegatedCalendar.primary && delegatedCalendar?.email) || undefined
: allCalendars.find((cal: Calendar) => cal.primary && cal.credentialId === credentialId)?.email;
const {
@@ -58,8 +58,8 @@ export class DestinationCalendarsService {
externalId,
userId,
primaryEmail ?? null,
dwdCalendar ? undefined : credentialId,
dwdCalendar ? domainWideDelegationCredentialId : undefined
delegatedCalendar ? undefined : credentialId,
delegatedCalendar ? delegationCredentialId : undefined
);
return {
@@ -282,13 +282,13 @@ describe("EventTypes WebhooksController (e2e)", () => {
});
});
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE) should fail to delete a webhook that does not exist", () => {
return request(app.getHttpServer())
.delete(`/v2/event-types/${eventType.id}/webhooks/1234453`)
.expect(404);
});
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not belong to user", () => {
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE) should fail to delete a webhook that does not belong to user", () => {
return request(app.getHttpServer())
.delete(`/v2/event-types/${otherEventType.id}/webhooks/${otherWebhook.id}`)
.expect(403);
+1 -1
View File
@@ -23,7 +23,7 @@ export class JwtService {
}
getIssuedAtTime() {
// divided by 1000 because iat (issued at time) is in seconds (not milliseconds) as informed by JWT speficication
// divided by 1000 because iat (issued at time) is in seconds (not milliseconds) as informed by JWT specification
return Math.floor(Date.now() / 1000);
}
}
@@ -5,7 +5,7 @@ import { IsEnum, IsNumber, IsString, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class CreateManagedUserData {
export class CreateManagedUserData {
@ApiProperty({
type: ManagedUserOutput,
})
@@ -274,7 +274,7 @@ describe("OAuth client WebhooksController (e2e)", () => {
.expect(403);
});
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (DELETE) should fail to delete a webhook that does not exist", () => {
return request(app.getHttpServer())
.delete(`/v2/oauth-clients/${oAuthClient.id}/webhooks/1234453`)
.expect(404);
@@ -1,6 +1,5 @@
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
@@ -2,12 +2,12 @@ import {
GoogleServiceAccountKeyInput,
ServiceAccountKeyValidator,
MicrosoftServiceAccountKeyInput,
} from "@/modules/organizations/dwd/inputs/service-account-key.input";
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsString, IsNotEmpty, ValidateNested, Validate } from "class-validator";
export class CreateDwdInput {
export class CreateDelegationCredentialInput {
@IsString()
@IsNotEmpty()
@ApiProperty()
@@ -2,12 +2,12 @@ import {
GoogleServiceAccountKeyInput,
MicrosoftServiceAccountKeyInput,
ServiceAccountKeyValidator,
} from "@/modules/organizations/dwd/inputs/service-account-key.input";
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
import { ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
import { Expose, plainToClass, Transform, Type } from "class-transformer";
import { IsBoolean, IsOptional, Validate, ValidateNested } from "class-validator";
export class UpdateDwdInput {
export class UpdateDelegationCredentialInput {
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
@@ -7,11 +7,11 @@ import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.g
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { UpdateDwdInput } from "@/modules/organizations/dwd/inputs/update-dwd.input";
import { CreateDwdOutput } from "@/modules/organizations/dwd/outputs/create-dwd.output";
import { DwdOutput } from "@/modules/organizations/dwd/outputs/dwd.output";
import { UpdateDwdOutput } from "@/modules/organizations/dwd/outputs/update-dwd.output";
import { OrganizationsDwdService } from "@/modules/organizations/dwd/services/organizations-dwd.service";
import { UpdateDelegationCredentialInput } from "@/modules/organizations/delegation-credentials/inputs/update-delegation-credential.input";
import { CreateDelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/create-delegation-credential.output";
import { DelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/delegation-credential.output";
import { UpdateDelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/update-delegation-credential.output";
import { OrganizationsDelegationCredentialService } from "@/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service";
import {
Controller,
UseGuards,
@@ -29,7 +29,7 @@ import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { CreateDwdInput } from "./inputs/create-dwd.input";
import { CreateDelegationCredentialInput } from "./inputs/create-delegation-credential.input";
@Controller({
path: "/v2/organizations/:orgId/delegation-credentials",
@@ -37,23 +37,27 @@ import { CreateDwdInput } from "./inputs/create-dwd.input";
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Delegation Credentials")
export class OrganizationsDwdController {
constructor(private readonly dwdService: OrganizationsDwdService) {}
export class OrganizationsDelegationCredentialController {
constructor(private readonly delegationCredentialService: OrganizationsDelegationCredentialService) {}
@Post("/")
@HttpCode(HttpStatus.CREATED)
@Roles("ORG_ADMIN")
@PlatformPlan("SCALE")
@ApiOperation({ summary: "Save delegation credentials for your organization." })
async createDwd(
async createDelegationCredential(
@Param("orgId", ParseIntPipe) orgId: number,
@GetUser() dwdServiceAccountUser: User,
@Body() body: CreateDwdInput
): Promise<CreateDwdOutput> {
const dwd = await this.dwdService.createDwd(orgId, dwdServiceAccountUser, body);
@GetUser() delegatedServiceAccountUser: User,
@Body() body: CreateDelegationCredentialInput
): Promise<CreateDelegationCredentialOutput> {
const delegationCredential = await this.delegationCredentialService.createDelegationCredential(
orgId,
delegatedServiceAccountUser,
body
);
return {
status: SUCCESS_STATUS,
data: plainToClass(DwdOutput, dwd, { strategy: "excludeAll" }),
data: plainToClass(DelegationCredentialOutput, delegationCredential, { strategy: "excludeAll" }),
};
}
@@ -61,16 +65,21 @@ export class OrganizationsDwdController {
@Roles("ORG_ADMIN")
@PlatformPlan("SCALE")
@ApiOperation({ summary: "Update delegation credentials of your organization." })
async updateDwd(
async updateDelegationCredential(
@Param("orgId", ParseIntPipe) orgId: number,
@GetUser() dwdServiceAccountUser: User,
@Body() body: UpdateDwdInput,
@GetUser() delegatedServiceAccountUser: User,
@Body() body: UpdateDelegationCredentialInput,
@Param("credentialId") credentialId: string
): Promise<UpdateDwdOutput> {
const dwd = await this.dwdService.updateDwd(orgId, credentialId, dwdServiceAccountUser, body);
): Promise<UpdateDelegationCredentialOutput> {
const delegationCredential = await this.delegationCredentialService.updateDelegationCredential(
orgId,
credentialId,
delegatedServiceAccountUser,
body
);
return {
status: SUCCESS_STATUS,
data: plainToClass(DwdOutput, dwd, { strategy: "excludeAll" }),
data: plainToClass(DelegationCredentialOutput, delegationCredential, { strategy: "excludeAll" }),
};
}
}
@@ -0,0 +1,21 @@
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";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { Module } from "@nestjs/common";
import { OrganizationsDelegationCredentialService } from "./services/organizations-delegation-credential.service";
@Module({
imports: [PrismaModule, StripeModule, RedisModule, MembershipsModule],
providers: [
OrganizationsDelegationCredentialService,
OrganizationsDelegationCredentialRepository,
OrganizationsRepository,
],
controllers: [OrganizationsDelegationCredentialController],
})
export class OrganizationsDelegationCredentialModule {}
@@ -0,0 +1,31 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
import { Prisma } from "@calcom/prisma/client";
@Injectable()
export class OrganizationsDelegationCredentialRepository {
constructor(private readonly dbRead: PrismaReadService) {}
async findById(delegationCredentialId: string) {
return this.dbRead.prisma.delegationCredential.findUnique({ where: { id: delegationCredentialId } });
}
async findByIdWithWorkspacePlatform(delegationCredentialId: string) {
return this.dbRead.prisma.delegationCredential.findUnique({
where: { id: delegationCredentialId },
include: { workspacePlatform: true },
});
}
async updateIncludeWorkspacePlatform(
delegationCredentialId: string,
data: Prisma.DelegationCredentialUncheckedUpdateInput
) {
return this.dbRead.prisma.delegationCredential.update({
where: { id: delegationCredentialId },
data,
include: { workspacePlatform: true },
});
}
}
@@ -1,20 +1,20 @@
import { DwdOutput } from "@/modules/organizations/dwd/outputs/dwd.output";
import { DelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/delegation-credential.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class UpdateDwdOutput {
export class CreateDelegationCredentialOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: DwdOutput,
type: DelegationCredentialOutput,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => DwdOutput)
data!: DwdOutput;
@Type(() => DelegationCredentialOutput)
data!: DelegationCredentialOutput;
}
@@ -14,7 +14,7 @@ class WorkspacePlatformDto {
slug!: string;
}
export class DwdOutput {
export class DelegationCredentialOutput {
@ApiProperty()
@IsString()
@Expose()
@@ -1,20 +1,20 @@
import { DwdOutput } from "@/modules/organizations/dwd/outputs/dwd.output";
import { DelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/delegation-credential.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class CreateDwdOutput {
export class UpdateDelegationCredentialOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: DwdOutput,
type: DelegationCredentialOutput,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => DwdOutput)
data!: DwdOutput;
@Type(() => DelegationCredentialOutput)
data!: DelegationCredentialOutput;
}
@@ -0,0 +1,93 @@
import { CreateDelegationCredentialInput } from "@/modules/organizations/delegation-credentials/inputs/create-delegation-credential.input";
import {
GoogleServiceAccountKeyInput,
MicrosoftServiceAccountKeyInput,
} 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 { User } from "@prisma/client";
import {
addDelegationCredential,
toggleDelegationCredentialEnabled,
encryptServiceAccountKey,
} from "@calcom/platform-libraries";
@Injectable()
export class OrganizationsDelegationCredentialService {
constructor(
private readonly organizationsDelegationCredentialRepository: OrganizationsDelegationCredentialRepository
) {}
async createDelegationCredential(
orgId: number,
delegatedServiceAccountUser: User,
body: CreateDelegationCredentialInput
) {
const delegationCredential = await addDelegationCredential({
input: body,
ctx: { user: { id: delegatedServiceAccountUser.id, organizationId: orgId } },
});
return delegationCredential;
}
async updateDelegationCredential(
orgId: number,
delegationCredentialId: string,
delegatedServiceAccountUser: User,
body: UpdateDelegationCredentialInput
) {
if (body.enabled !== undefined) {
await this.updateDelegationCredentialEnabled(
orgId,
delegationCredentialId,
delegatedServiceAccountUser,
body.enabled
);
}
if (body.serviceAccountKey !== undefined) {
await this.updateDelegationCredentialServiceAccountKey(delegationCredentialId, body.serviceAccountKey);
}
const delegationCredential =
await this.organizationsDelegationCredentialRepository.findByIdWithWorkspacePlatform(
delegationCredentialId
);
if (!delegationCredential) {
throw new NotFoundException(`DelegationCredential with id ${delegationCredentialId} not found`);
}
return delegationCredential;
}
async updateDelegationCredentialEnabled(
orgId: number,
delegationCredentialId: string,
delegatedServiceAccountUser: User,
enabled: boolean
) {
const handlerUser = {
id: delegatedServiceAccountUser.id,
email: delegatedServiceAccountUser.email,
organizationId: orgId,
};
const handlerBody = { id: delegationCredentialId, enabled };
const delegationCredential = await toggleDelegationCredentialEnabled(handlerUser, handlerBody);
return delegationCredential;
}
async updateDelegationCredentialServiceAccountKey(
delegationCredentialId: string,
serviceAccountKey: GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput
) {
const encryptedServiceAccountKey = encryptServiceAccountKey(serviceAccountKey);
const delegationCredential =
await this.organizationsDelegationCredentialRepository.updateIncludeWorkspacePlatform(
delegationCredentialId,
{
serviceAccountKey: encryptedServiceAccountKey,
}
);
return delegationCredential;
}
}
@@ -1,17 +0,0 @@
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OrganizationsDwdController } from "@/modules/organizations/dwd/organizations-dwd.controller";
import { OrganizationsDwdRepository } from "@/modules/organizations/dwd/organizations-dwd.repository";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { Module } from "@nestjs/common";
import { OrganizationsDwdService } from "./services/organizations-dwd.service";
@Module({
imports: [PrismaModule, StripeModule, RedisModule, MembershipsModule],
providers: [OrganizationsDwdService, OrganizationsDwdRepository, OrganizationsRepository],
controllers: [OrganizationsDwdController],
})
export class OrganizationsDwdModule {}
@@ -1,28 +0,0 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
import { Prisma } from "@calcom/prisma/client";
@Injectable()
export class OrganizationsDwdRepository {
constructor(private readonly dbRead: PrismaReadService) {}
async findById(dwdId: string) {
return this.dbRead.prisma.domainWideDelegation.findUnique({ where: { id: dwdId } });
}
async findByIdWithWorkspacePlatform(dwdId: string) {
return this.dbRead.prisma.domainWideDelegation.findUnique({
where: { id: dwdId },
include: { workspacePlatform: true },
});
}
async updateIncludeWorkspacePlatform(dwdId: string, data: Prisma.DomainWideDelegationUncheckedUpdateInput) {
return this.dbRead.prisma.domainWideDelegation.update({
where: { id: dwdId },
data,
include: { workspacePlatform: true },
});
}
}
@@ -1,62 +0,0 @@
import { CreateDwdInput } from "@/modules/organizations/dwd/inputs/create-dwd.input";
import {
GoogleServiceAccountKeyInput,
MicrosoftServiceAccountKeyInput,
} from "@/modules/organizations/dwd/inputs/service-account-key.input";
import { UpdateDwdInput } from "@/modules/organizations/dwd/inputs/update-dwd.input";
import { OrganizationsDwdRepository } from "@/modules/organizations/dwd/organizations-dwd.repository";
import { DwdOutput } from "@/modules/organizations/dwd/outputs/dwd.output";
import { Injectable, NotFoundException } from "@nestjs/common";
import { User } from "@prisma/client";
import { addDwd, toggleDwdEnabled, encryptServiceAccountKey } from "@calcom/platform-libraries";
@Injectable()
export class OrganizationsDwdService {
constructor(private readonly organizationsDwdRepository: OrganizationsDwdRepository) {}
async createDwd(orgId: number, dwdServiceAccountUser: User, body: CreateDwdInput) {
console.log("asap body", JSON.stringify(body, null, 2));
const dwd = await addDwd({
input: body,
ctx: { user: { id: dwdServiceAccountUser.id, organizationId: orgId } },
});
return dwd;
}
async updateDwd(orgId: number, dwdId: string, dwdServiceAccountUser: User, body: UpdateDwdInput) {
if (body.enabled !== undefined) {
await this.updateDwdEnabled(orgId, dwdId, dwdServiceAccountUser, body.enabled);
}
if (body.serviceAccountKey !== undefined) {
await this.updateDwdServiceAccountKey(dwdId, body.serviceAccountKey);
}
const dwd = await this.organizationsDwdRepository.findByIdWithWorkspacePlatform(dwdId);
if (!dwd) {
throw new NotFoundException(`DWD with id ${dwdId} not found`);
}
return dwd;
}
async updateDwdEnabled(orgId: number, dwdId: string, dwdServiceAccountUser: User, enabled: boolean) {
const handlerUser = {
id: dwdServiceAccountUser.id,
email: dwdServiceAccountUser.email,
organizationId: orgId,
};
const handlerBody = { id: dwdId, enabled };
const dwd = await toggleDwdEnabled(handlerUser, handlerBody);
return dwd;
}
async updateDwdServiceAccountKey(
dwdId: string,
serviceAccountKey: GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput
) {
const encryptedServiceAccountKey = encryptServiceAccountKey(serviceAccountKey);
const dwd = await this.organizationsDwdRepository.updateIncludeWorkspacePlatform(dwdId, {
serviceAccountKey: encryptedServiceAccountKey,
});
return dwd;
}
}
@@ -187,6 +187,9 @@ describe("Organizations Event Types Endpoints", () => {
type: "integration",
integration: "cal-video",
},
{
type: "organizersDefaultApp",
},
],
schedulingType: "COLLECTIVE",
hosts: [
@@ -4,6 +4,7 @@ import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { transformTeamLocationsApiToInternal } from "@calcom/platform-libraries/event-types";
import {
CreateTeamEventTypeInput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
@@ -11,6 +12,13 @@ import {
} from "@calcom/platform-types";
import { SchedulingType } from "@calcom/prisma/client";
export type TransformedCreateTeamEventTypeInput = Awaited<
ReturnType<InstanceType<typeof InputOrganizationsEventTypesService>["transformInputCreateTeamEventType"]>
>;
export type TransformedUpdateTeamEventTypeInput = Awaited<
ReturnType<InstanceType<typeof InputOrganizationsEventTypesService>["transformInputUpdateTeamEventType"]>
>;
@Injectable()
export class InputOrganizationsEventTypesService {
constructor(
@@ -81,9 +89,17 @@ export class InputOrganizationsEventTypesService {
teamId: number,
inputEventType: CreateTeamEventTypeInput_2024_06_14
) {
const { hosts, assignAllTeamMembers, ...rest } = inputEventType;
const { hosts, assignAllTeamMembers, locations, ...rest } = inputEventType;
const eventType = this.inputEventTypesService.transformInputCreateEventType(rest);
const defaultLocations: CreateTeamEventTypeInput_2024_06_14["locations"] = [
{
type: "integration",
integration: "cal-video",
},
];
const children = await this.getChildEventTypesForManagedEventType(null, inputEventType, teamId);
const metadata =
@@ -100,6 +116,7 @@ export class InputOrganizationsEventTypesService {
: this.transformInputHosts(hosts, inputEventType.schedulingType)
: undefined,
assignAllTeamMembers,
locations: this.transformInputTeamLocations(locations || defaultLocations),
metadata,
children,
};
@@ -112,7 +129,7 @@ export class InputOrganizationsEventTypesService {
teamId: number,
inputEventType: UpdateTeamEventTypeInput_2024_06_14
) {
const { hosts, assignAllTeamMembers, ...rest } = inputEventType;
const { hosts, assignAllTeamMembers, locations, ...rest } = inputEventType;
const eventType = await this.inputEventTypesService.transformInputUpdateEventType(rest, eventTypeId);
const dbEventType = await this.teamsEventTypesRepository.getTeamEventType(teamId, eventTypeId);
@@ -132,6 +149,7 @@ export class InputOrganizationsEventTypesService {
: undefined,
assignAllTeamMembers,
children,
locations: locations ? this.transformInputTeamLocations(locations) : undefined,
};
return teamEventType;
@@ -179,6 +197,10 @@ export class InputOrganizationsEventTypesService {
return eventType?.children.map((child) => child.userId).filter((id) => !!id) as number[];
}
transformInputTeamLocations(inputLocations: CreateTeamEventTypeInput_2024_06_14["locations"]) {
return transformTeamLocationsApiToInternal(inputLocations);
}
async getOwnersForManagedEventType(userIds: number[]) {
const users = await this.usersRepository.findByIdsWithEventTypes(userIds);
@@ -230,7 +252,11 @@ export class InputOrganizationsEventTypesService {
const membersIds = await this.teamsRepository.getTeamMembersIds(teamId);
const invalidHosts = hosts.filter((host) => !membersIds.includes(host.userId));
if (invalidHosts.length) {
throw new NotFoundException(`Invalid hosts: ${invalidHosts.join(", ")}`);
throw new NotFoundException(
`Invalid hosts: ${invalidHosts
.map((host) => host.userId)
.join(", ")} are not members of team with id ${teamId}.`
);
}
}
}
@@ -1,5 +1,9 @@
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
import {
TransformedCreateTeamEventTypeInput,
TransformedUpdateTeamEventTypeInput,
} from "@/modules/organizations/event-types/services/input.service";
import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
@@ -7,8 +11,8 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, Logger } from "@nestjs/common";
import { createEventType } from "@calcom/platform-libraries";
import { InputTeamEventTransformed_2024_06_14 } from "@calcom/platform-types";
// eslint-disable-next-line @typescript-eslint/no-var-requires
import { createEventType } from "@calcom/platform-libraries/event-types";
@Injectable()
export class OrganizationsEventTypesService {
@@ -26,14 +30,16 @@ export class OrganizationsEventTypesService {
user: UserWithProfile,
teamId: number,
orgId: number,
body: InputTeamEventTransformed_2024_06_14
body: TransformedCreateTeamEventTypeInput
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
const eventTypeUser = await this.getUserToCreateTeamEvent(user, orgId);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { hosts, children, destinationCalendar, ...rest } = body;
const { eventType: eventTypeCreated } = await createEventType({
input: { teamId: teamId, ...rest },
input: {
teamId: teamId,
...rest,
},
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -57,6 +63,7 @@ export class OrganizationsEventTypesService {
organization: { isOrgAdmin },
profile: { id: profileId || null },
metadata: user.metadata,
email: user.email,
};
}
@@ -87,7 +94,7 @@ export class OrganizationsEventTypesService {
async updateTeamEventType(
eventTypeId: number,
teamId: number,
body: InputTeamEventTransformed_2024_06_14,
body: TransformedUpdateTeamEventTypeInput,
user: UserWithProfile
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user);
@@ -11,7 +11,7 @@ import { OrganizationAttributesService } from "@/modules/organizations/attribute
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository";
import { OrganizationsAttributesOptionsController } from "@/modules/organizations/attributes/options/organizations-attributes-options.controller";
import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service";
import { OrganizationsDwdModule } from "@/modules/organizations/dwd/organizations-dwd.module";
import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module";
import { OrganizationsEventTypesController } from "@/modules/organizations/event-types/organizations-event-types.controller";
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
@@ -65,7 +65,7 @@ import { Module } from "@nestjs/common";
EventTypesModule_2024_06_14,
TeamsEventTypesModule,
TeamsModule,
OrganizationsDwdModule,
OrganizationsDelegationCredentialModule,
OrganizationsOrganizationsModule,
OrganizationsTeamsRoutingFormsModule,
],
@@ -46,7 +46,11 @@ export class OrganizationsOrganizationsController {
@Post()
@Roles("ORG_ADMIN")
@PlatformPlan(SCALE)
@ApiOperation({ summary: "Create an organization within an organization" })
@ApiOperation({
summary: "Create an organization within an organization",
description:
"Requires the user to have at least the 'ORG_ADMIN' role within the organization. Additionally, for platform, the plan must be 'SCALE' or higher to access this endpoint.",
})
async createOrganization(
@Param("orgId", ParseIntPipe) managerOrganizationId: number,
@GetUser() authUser: ApiAuthGuardUser,
@@ -67,7 +71,11 @@ export class OrganizationsOrganizationsController {
@Roles("ORG_ADMIN")
@PlatformPlan(SCALE)
@Get("/:managedOrganizationId")
@ApiOperation({ summary: "Get an organization within an organization" })
@ApiOperation({
summary: "Get an organization within an organization",
description:
"Requires the user to have at least the 'ORG_ADMIN' role within the organization. Additionally, for platform, the plan must be 'SCALE' or higher to access this endpoint.",
})
@UseGuards(IsManagedOrgInManagerOrg)
async getOrganization(
@Param("managedOrganizationId", ParseIntPipe) managedOrganizationId: number
@@ -82,7 +90,11 @@ export class OrganizationsOrganizationsController {
@Roles("ORG_ADMIN")
@PlatformPlan(SCALE)
@Get("/")
@ApiOperation({ summary: "Get all organizations within an organization" })
@ApiOperation({
summary: "Get all organizations within an organization",
description:
"Requires the user to have at least the 'ORG_ADMIN' role within the organization. Additionally, for platform, the plan must be 'SCALE' or higher to access this endpoint.",
})
async getOrganizations(
@Param("orgId", ParseIntPipe) managerOrganizationId: number
): Promise<GetManagedOrganizationsOutput> {
@@ -98,7 +110,11 @@ export class OrganizationsOrganizationsController {
@Roles("ORG_ADMIN")
@PlatformPlan(SCALE)
@Patch("/:managedOrganizationId")
@ApiOperation({ summary: "Update an organization within an organization" })
@ApiOperation({
summary: "Update an organization within an organization",
description:
"Requires the user to have at least the 'ORG_ADMIN' role within the organization. Additionally, for platform, the plan must be 'SCALE' or higher to access this endpoint.",
})
@UseGuards(IsManagedOrgInManagerOrg)
@HttpCode(HttpStatus.OK)
async updateOrganization(
@@ -119,7 +135,11 @@ export class OrganizationsOrganizationsController {
@Roles("ORG_ADMIN")
@PlatformPlan(SCALE)
@Delete("/:managedOrganizationId")
@ApiOperation({ summary: "Delete an organization within an organization" })
@ApiOperation({
summary: "Delete an organization within an organization",
description:
"Requires the user to have at least the 'ORG_ADMIN' role within the organization. Additionally, for platform, the plan must be 'SCALE' or higher to access this endpoint.",
})
@UseGuards(IsManagedOrgInManagerOrg)
async deleteOrganization(
@Param("managedOrganizationId", ParseIntPipe) managedOrganizationId: number
@@ -94,7 +94,7 @@ describe("Organizations Schedules Endpoints", () => {
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/schedules`).expect(403);
});
it("should mot be able to get user schedules", async () => {
it("should not be able to get user schedules", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/users/${user.id}/schedules/`)
.expect(403);

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