Compare commits

...
152 Commits
Author SHA1 Message Date
Alex van Andel d431607125 chore: Release 4.2.2 (#15568)
release.yml / release (push) Canceled after 0s
2024-06-25 21:45:42 +00:00
Joe Au-Yeung 72253021ef Include business days plug and window types (#15569) 2024-06-25 16:23:26 +00:00
Morgan be0cc31d63 fix: IsOrgGuard apiv2 casing (#15566) 2024-06-25 14:13:07 +00:00
sean-brydon 38e67ccc79 Use flex box properly and use description as intended (#15565) 2024-06-25 12:52:53 +00:00
Hariom Balhara 3fe26f19ff Fix git-setup.sh (#15560) 2024-06-25 11:45:33 +00:00
Crowdin Bot f38f98a0b6 New Crowdin translations by Github Action 2024-06-25 11:24:53 +00:00
Morgan b4d5f511b5 chore: IsOrg guard api v2 (#15563)
* chore: is org guard api v2

* fixup! chore: is org guard api v2

* fixup! fixup! chore: is org guard api v2
2024-06-25 10:34:00 +00:00
86fc5161b6 refactor: loadUsers and move to separate file (#15532)
* refactor: loadUsers and move to separate file

* fix: improvement

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2024-06-25 07:02:45 +00:00
Alex van Andel dbf4be27ea fix: SAML: invalid_code issue, missing user error signing up (#15522)
* fix: Error thrown due to missing user

* chore: Remove unnecessary logs

* fix: Remove PageWrapper in order to prevent double signIn

* Change profile typing to optional

* further type fix

* further type fix..
2024-06-24 14:53:23 +00:00
7b69a6ab9c feat: add monthly annual billing org form (#15520)
* Add annuall billing options

* added payment tests

* Add conitional + tests

* Fix types + use correct ID

* Fix type error

* fix type check

* Assign default to monthly billing period

* cleanup

* chore: rename fn and params

* Prevent ability to deselect toggle group

* Calculate yearly price

* Fix TS error

* revese billingPeriod

* reformat how we handle billingPeriod

* Improve type

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-06-24 14:03:27 +00:00
f897c680a4 fix: zoom shows up as location when expired instead of cal (#15153)
* fix:location doesn't match with meeting url when defaults to calvideo on app credentials expire

* revert temporary logs

* chore: use DailyLocationType

* tests:modify exisiting tests

---------

Co-authored-by: Shaik-Sirajuddin <sirajudddinshaik30@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-06-24 12:22:12 +00:00
ac05693694 fix: Transaltion for Dashboard Theme box is not available in all languages #15480 (#15484)
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-06-24 11:16:35 +00:00
Alex van AndelandKeith Williams 3174b55e21 chore: Attempt reduction of Sentry size by treeshaking tracing (#15527)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-06-24 11:16:19 +00:00
Lauris SkraucisandKeith Williams 08f4a48774 fix: v2 event-types versioning (#15549)
* Added a log for pull_request

* Added labels logging

* Using labels straight from event PR object

* Converting to JSON

* Switching to use payload

* Fixed issue with undefined pr

* Fixed non-mapping issue

* Removed the types

* Added another cache key segment using the head commit sha

* Added separate workflow for labeled action

* Fixed syntax error

* Fixing payload issue

* Added log

* logging full object

* Put e2e back in the names to help find them

* Limited logging

* fix: v2 even-types versioning

* test: old v2 even-types request with VERSION_2024_06_11

* test: old v2 even-types request with VERSION_2024_04_15

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-06-24 10:43:58 +00:00
Keith Williams 928dd49965 chore: Remove check for labeled action (#15547) 2024-06-24 06:44:37 +00:00
Keith Williams 47a8ba1eaa chore: streamline ready-for-e2e label check for PRs (#15545)
* Added a log for pull_request

* Added labels logging

* Using labels straight from event PR object

* Converting to JSON

* Switching to use payload

* Fixed issue with undefined pr

* Fixed non-mapping issue

* Removed the types

* Removed issue with run-jobs

* Put back the types
2024-06-23 15:38:59 +00:00
Keith Williams c4e78c213b chore: Add GH actions test branch (#15541)
* chore: Add GH actions test branch

* Added a log of the pull_request object

* Removed log

* Update pr.yml
2024-06-22 17:59:15 -03:00
Keith Williams a31cf9963d chore: Make ready-for-e2e label check more robust (#15536)
* chore: Make ready-for-e2e label check more robust

* fixed owner issue

* Try that again

* multi-step check for PR

* added catch

* Using the output from changes

* more fixes

* logging the sha

* Attemptig to fix the sha problem

* Cleanup

* Cleanup

* Testing with the same sha reference as checkout

* Reverted back

* cleanup
2024-06-22 14:04:37 +00:00
Keith Williams 7ff255d1e7 fix: New CI workflow flaws in detecting the ready-for-e2e label on PRs (#15534)
* logging the event

* Added logging

* Trying a different way to reference the event names

* Trying to fix string issues

* Clean up
2024-06-22 07:59:40 +00:00
Keith WilliamsandJoe Au-Yeung 4a88051531 chore: Only run E2E suite when ready-for-e2e label added (#15529)
* chore: Improve CI workflow

* Made a new pre-merge flow that runs the builds and e2e

* Refactored back into 1 file

* Adding another condition for checking the ready-for-e2e

* syntax

* More syntax

* Change e2e-required to only run when ready so it doesn't fail

* A different attempt at getting the ready-for-e2e label

* Supporting PRs and workflow_dispatch

* logging the branch

* hardcoding to test

* Trying the rest call

* Added logging

* Check for the label in the same step

* More logging

* Added job-level output

* Reverted back to 'true'

* Moved the e2e required check into the main required check

* More logging

* Peeling the branch off of the ref

* Fix the ref

* Trying a separate workflow for labeling

* Updated name for the labeled file

* Checking label events to avoid unnecessary runs

* Got rid of the extra check to see if required should run

* Upgraded old actions running Node 16

* Updated the pre-reqs for running jobs

* Removed the dependency of API builds before starting E2E suites

* Removed extra production build used by analyze job

* Fixed the run-e2e check

* Using the pull request object when available

* Checking for null PR

* Added check for no event

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-06-22 00:52:27 +00:00
VijayandJoe Au-Yeung 765b0b44f8 fix: #15487 select arbitary start time (#15523)
* update for any startdate

* update end time if less than start

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-06-21 18:25:57 +00:00
Amit SharmaandUdit Takkar 10f8b40348 fix: Webex app not working (#15485)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-06-21 18:14:15 +00:00
c9a9f70801 fix: create instant meeting error if DAILY_SCALE_PLAN is false (#15508)
* fix: create instant meeting error due to enable_recording

* fix: check scale plan

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-06-21 14:45:17 +00:00
Alex van Andel c2d7054ec4 fix: isChildEvent issue + loader is shown forever (#15524) 2024-06-21 13:07:46 +00:00
Pranav Rajveer 89ac9a3d31 fix: Dashboard theme language (#15486) 2024-06-21 11:46:14 +00:00
7d9c7a8c22 feat: Add Hungarian support (#14484)
* add: Hungarian support

* Update next-i18next.config.js

* Fix test

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-06-21 11:29:40 +00:00
Lauris Skraucis 48e7efccb2 fix: AvailabilitySettings atom closing override window button (#15518)
* fix: close override window button

* fix: test
2024-06-21 11:06:47 +00:00
Lauris SkraucisandMorgan 0072aba55d refactor: v2 event-types (#15457)
* refactor: UserAvatarGroupWithOrg.tsx remove unecessary profile prop

* chore: define event-type types in platform/types

* feat: event-type transformes in lib and re-exported through platform-libraries

* chore: define user types in platform/types

* chore: add users endpoint to platform-constants

* refactor: app-store use narrowed down booker event type

* chore: version old event-types module

* fix: old event-types e2e test

* fix: libraries add missing export

* fix: reset libraries version to 0

* feat: new event-types module and users endpoints

* feat: make booker atom work with v2 event-types

* updating event type

* refactor: remove guard for get event-types

* refactor: move private hook to public

* Revert "refactor: remove guard for get event-types"

This reverts commit d41204069f1d5bb1305b388ce53399f9175f4249.

* Revert "refactor: move private hook to public"

This reverts commit 09322e8fba102e57104959f79ed6bfa9a677dc9d.

* refactor: get by username and slug in /event-types

* remove console log

* feat: locations handle displayEventPublicly

* test e2e event-types

* revert: user output from types

* refactor: event-types hooks have private and public folders

* refactor: require event-type.slug in input

* refactor: demand that all labels of booking fields are unique

* refactor: remove unused import

* refactor: only have email and name by default in booker

* refactor: add booking field slug

* fix: display event-type location publicly / hide it in booker

* fix: display event-type location publicly / hide it in booker

* fix: packages/lib tests

* fix: typescript in e2e test

* fix: dynamic event types input

* refactor: use IsIn instead of IsEnum

* refactor: simplify getEventTypes

* fix: use ApiAuthGuard instead of AccessTokenGuard

* chore: export more stuff from libraries

* refactor: SchedulingTypeEnum and Type

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-06-21 10:49:12 +00:00
Vedant JainandPeer Richelsen c7a96e1b43 [CAL-3940] randomize Tip Sidebar (#15469)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-06-21 09:49:43 +00:00
Morgan 0288ea931f fix: license key env type api-v2 (#15519) 2024-06-21 09:04:43 +00:00
sean-brydonandJoe Au-Yeung c958b6017a fix: modal logic for assignment reminder (#15488)
* fix: modal logic for assignemtn reminder

* Fix: joe feedback

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-06-21 07:48:14 +00:00
Keith Williams ffe03be018 chore: v4.2.1 (#15514)
release.yml / release (push) Canceled after 0s
2024-06-20 20:05:55 +00:00
Lauris Skraucis f760a8ac26 refactor: only check platform billing for platforms (#15507) 2024-06-20 10:27:42 +00:00
smitgolandHariom d0a7eb12a0 fix: your name translation (#14863)
* your name translation fixed

* do translation outside Components as that is needed by all components. Allows the code to stay DRY

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-06-20 08:39:21 +00:00
Morgan 9a473d5b55 chore: auth on api-v2 with api-key (#15455)
* chore: enable apiv2 auth with api keys

* chore: enable apiv2 auth with api keys

* chore: enable apiv2 auth with api keys

* chore: enable apiv2 auth with api keys

* chore: enable apiv2 auth with api keys

* fixup! chore: enable apiv2 auth with api keys

* fixup! Merge branch 'chore-apiv2-auth-api-key' of github.com:calcom/cal.com into chore-apiv2-auth-api-key

* fixup! fixup! Merge branch 'chore-apiv2-auth-api-key' of github.com:calcom/cal.com into chore-apiv2-auth-api-key

* fixup! fixup! fixup! Merge branch 'chore-apiv2-auth-api-key' of github.com:calcom/cal.com into chore-apiv2-auth-api-key
2024-06-19 16:00:22 +00:00
sean-brydonandKeith Williams c5a83f4df2 fix: correctly assign status code on rate limit error (#15435)
* set status code

* Add tests

* Update apps/api/v1/lib/helpers/rateLimitApiKey.test.ts

* Update apps/api/v1/lib/helpers/rateLimitApiKey.test.ts

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-06-19 15:03:28 +00:00
sean-brydon 3e175dbca3 Remove comment (#15493) 2024-06-19 14:17:32 +00:00
762b995172 fix: event type uses calvideo as location instead of user default (#15442)
* fix_event_type_created_with_calvideo_instead_of_user_default_conferencing_app

* test: add tests for getDefaultLocation

---------

Co-authored-by: Shaik-Sirajuddin <sirajudddinshaik30@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-06-19 14:00:06 +00:00
d2d7453962 feat: add date overrides to availability settings atom (#15459)
* remove console.log

* fixup: logic to convert overrides was not calling functions

* refactor handleSubmit function

* add portal to platform dialog component

* cleanup

* add props to enable and disable date overrides

* show date overrides for availability settings atom

* fix: date overrides formatting for api request

---------

Co-authored-by: Rajiv Sahal <rajivsahal@Rajivs-MacBook-Pro.local>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-06-19 12:19:25 +00:00
Omar López 87d2ec9e75 chore: v4.2.0 (#15482)
release.yml / release (push) Canceled after 0s
* v4.1.13

* Update package.json
2024-06-18 19:42:25 +00:00
Rushikeshandsean-brydon 226686d4dd fix: Large padding on small screens for embedded booking widgets (#15463)
* fix: large padding on small screens for embedded booking widgets

* removed unused

* cssConflict - 'border-booker-width' applies the same CSS properties as 'border'.

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-06-18 10:35:22 +00:00
Rajiv SahalandRajiv Sahal 518ab82e3b shift pencil icon after input to make it wider (#15460)
Co-authored-by: Rajiv Sahal <rajivsahal@Rajivs-MacBook-Pro.local>
2024-06-18 08:25:18 +00:00
Peer Richelsen 532b78a9b3 added empty screen for missing platform plan (#15467) 2024-06-18 07:37:48 +00:00
1251091300 fix: Offer seats events invites everyone regardless their availability schedule (#15171)
* fix: Offer seats events invites everyone regardless their availibility schedule

* chore: add test

* uncomment

* remove timeout

* only check when first seat

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-06-17 15:30:41 +00:00
Rajdeep Das ce1459011a fix: unexpected scrolling on event-types page (#15452) 2024-06-17 14:25:23 +00:00
Joe Au-YeungandUdit Takkar e1bdac077a fix: Seat Attendee Rescheduling Logic (#14784)
* When rescheduling to new time slot don't trigger reschedule logic

* Add unique iCalUID when rescheduling to new slot

* Generating iCalUID use v4 uuid

* When moving an attendee to a new booking. Only move the attendee

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-06-15 08:57:52 +00:00
Vikas PatilandHariom d666f2926c feat: umami app added (#14783)
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-06-14 13:42:14 +00:00
Vikas PatilandPeer Richelsen 745f0b0872 feat: twipla app added (#14780)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-06-14 12:34:59 +00:00
65f50e9f25 chore: return accessTokenExpiresAt in oauth exchange and refresh (#15425)
* chore: return accessTokenExpiresAt in oauth exchange and refresh

* fixup! chore: return accessTokenExpiresAt in oauth exchange and refresh

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-06-14 11:25:27 +00:00
Alex van Andel 54162f8568 chore: Release v4.1.12 (#15432)
release.yml / release (push) Canceled after 0s
2024-06-13 22:42:18 +00:00
sean-brydon 0f4b9b2a6e fix (#15431) 2024-06-13 22:18:36 +00:00
Alex van AndelandJack W 70b4e3a9c6 Use "standalone" output to build Next.js as a standalone app when being built e.g. in Docker. (#15405)
Co-authored-by: Jack W <jackweatherilt@outlook.com>
2024-06-13 14:44:49 +00:00
ab44d5b92c add zod.ts for google meet (#15203)
Co-authored-by: Shaik-Sirajuddin <sirajudddinshaik30@gmail.com>
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-06-13 14:00:57 +00:00
Peer Richelsen bed08c1e4f Update semantic-pull-requests.yml (#15421) 2024-06-13 14:00:44 +00:00
Alex van Andel 1e0f7f433f chore: Upgrade Sentry in v1, v2 & web (#15396)
* chore: Upgrade Sentry in v1, v2 & web

* fix: Keep ./instrument to be the first line

* Remove eslint-plugin-import (fixed in prettier)

* Optimistic removal pending some feedback

* Simplified and fixed sentry wrapper

* Create instrumentation.ts also in APIv1

* sentry key in main nextConfig is deprecated
2024-06-13 14:00:27 +00:00
Udit Takkar 0cc7a9fc22 refactor: recording and transcript webhook (#15419)
* refactor: recording and transcript webhook

* refactor: improve code and test
2024-06-13 13:59:57 +00:00
b03b878199 fix: reschedule and cancel unconfirmed event from email (#15013)
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-06-13 09:09:24 +00:00
7a560cf09b chore: added platform admin to seed.ts (#15119)
* initial commit for platform seed

* seed platform team

* fix type checks

* refactors

* fix merge conflicts

* add seed for setting up platform user and team

* fixup

---------

Co-authored-by: Rajiv Sahal <rajivsahal@Rajivs-MacBook-Pro.local>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2024-06-13 06:53:37 +00:00
Hariom Balhara 58ad441223 fix: margin top in booker embed (#15412) 2024-06-12 14:08:04 +00:00
Alex van Andel d8de919acb Revert "fix: bookings made via API don't include the specified location (#15067)" (#15417)
This reverts commit 219b13d053.
2024-06-12 12:34:56 +00:00
Lauris Skraucis d699b33dcb chore: v2 managed user inform when access token expires (#15414) 2024-06-12 09:05:35 +00:00
Alex van Andel c071df4cd6 release: v4.1.11 (#15406)
release.yml / release (push) Canceled after 0s
2024-06-11 18:23:09 +00:00
219b13d053 fix: bookings made via API don't include the specified location (#15067)
* fix booking via api not picking default location when unspecified

* test:add tests for default location of event for booking

* fix:failing tests

---------

Co-authored-by: Shaik-Sirajuddin <sirajudddinshaik30@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-06-11 17:06:43 +00:00
Pranav RajveerandPeer Richelsen 5e09281974 fix: fixed the padding at the footer in /signup (#15394)
* fixed the padding at the footer in /signup

* reverted /auth/login Link style to original

* Update apps/web/pages/signup.tsx

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
2024-06-11 16:45:11 +00:00
Udit TakkarandJoe Au-Yeung b66d36a98e feat: generate transcription from recording and API endpoint (#15358)
* feat: generate transcription from recording

* fix: api

* chore: undo

* tests: add unit tests

* chore: refactor move zod type

* fix: add more security check

* chore: update test

* chore: type err

* chore: test

* chore: fix tyoe

* fix: build err

* chore: add try .. catch

* chore: update desc

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-06-11 15:41:56 +00:00
Lauris Skraucis 68ce9524eb refactor: v2 schedules (#15403)
* refactor: platform-types schedule types

* refactor: lib add bidirectional schedule transformers api <-> atom

* refactor: trpc handlers use renamed lib transformers

* refactor: platform-libraries export new bi-directional transformers

* chore: v2 update platform-libraries

* chore: enable also old platform-libraries

* fix: platform-types support previous types

* refactor: versioning date

* chore: version old schedules as schedules_2024_04_15 module

* chore: version old schedules as schedules_2024_04_15 module

* feat: new 2024_06_11 schedules module

* feat: atoms use refactored schedule api

* refactor: remove ?for=atom in schedules hooks

* fix: unit test
2024-06-11 14:35:53 +00:00
Morgan 357a42b512 fix: atoms useCheck and bump atoms version (#15390) 2024-06-11 13:38:39 +00:00
33d7da88bf feat: go to Assignment tab after creation and warn if existing without assigning (#14882)
* feat: go to Assignment tab after creation and warn if existing without assigning

* Update AssignmentWarningDialog.tsx

* fix: some changes

* fix: some updates

* Update EventTeamTab.tsx

* update

* update

* update test

* some fixes

* refactor one test and update

* Update managed-event-types.e2e.ts

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
2024-06-11 09:27:07 +00:00
Martin Sione cd888be61e feat: Add VariableNode and AddVariablesPlugin (#15293) 2024-06-10 13:46:50 +00:00
Joe Au-Yeung 06231ec299 Add better error logging (#15383) 2024-06-10 10:10:19 +00:00
sean-brydonandUdit Takkar e38705c3e8 add missing migration (#15369)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-06-08 14:20:25 +00:00
Hichem FantarandUdit Takkar 5c95268d1a feat: Datepicker transitions (#15313)
* fix: Kbar component styles

Fix Kbar styles on active state

* refactor: Add transition to event type list item

The commit refactors the event type list item component by adding a transition effect to improve the user experience.

* refactor: Add transition effect to DatePicker component

Add transition effect to improve the user experience in the DatePicker component.

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-06-08 14:05:49 +00:00
Kartik Saini b310eb1a7e feat: Remove minimum of 2 teams for making an Org (#15267)
* feat: Remove min of 2 teams for making an Org

* Set ORG_MINIMUM_PUBLISHED_TEAMS_SELF_SERVE to 0
2024-06-07 14:30:36 +00:00
051510fa36 fix sidebar height not growing (#15223)
Co-authored-by: Rajiv Sahal <rajivsahal@Rajivs-MacBook-Pro.local>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-06-07 10:19:07 +00:00
Peer Richelsen 7d1226b823 Delete apps/web/public/.well-known directory (#15362) 2024-06-06 19:59:15 +00:00
Morgan 7732a22c49 fix: event-types controller params validation (#15353) 2024-06-06 19:58:46 +00:00
sean-brydonandPeer Richelsen 2a41cd0770 feat: license creation admin PR (#15024)
* WIP: UI created to see if it matches RFC idea

* Remove schema as we validate on the backend with old+new key schemas

* Add comment about proxy URL request when on older versions

* feat: create custom axios instance to use signature middleware

* fix: update form to respect new schema

* Add cal signature token to trubo env

* use backend mutation for creating license keys

* add loading state to creation button

* update form to have a success state

* fix type error for console.warning :')

* use api version v1

* restore checkLicense from main

* Add env vars to example

* nit: rename page export

* use US spelling

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-06-06 18:34:32 +00:00
a574e5e60f fix: trigger workflow only for first recurring event (#15128)
* fix isFirstRecurringEvent param

* code clean up

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-06-06 18:34:17 +00:00
Keith Williams 0b6508379b v4.1.10 (#15359)
release.yml / release (push) Canceled after 0s
2024-06-06 18:25:42 +00:00
Peer Richelsen f6930df87d chore: removed next app dir banner (#15356)
* removed next app dir banner

* yarn lock
2024-06-06 16:55:32 +00:00
7d4e8e2210 feat: outlook 365 connect atom (#15318)
* custom for office 365 connect atom

* office 365 connect atom

* add office 365 atom to package exports

* fixup

* fixup

* fixup fixup

* refactors

* chore: refactor connect atoms

* fixup

* chore: refactor connect, enable custom redir

* fixup! Merge branch 'outlook-365-calendar-frontend' of github.com:calcom/cal.com into outlook-365-calendar-frontend

* fixup! fixup! Merge branch 'outlook-365-calendar-frontend' of github.com:calcom/cal.com into outlook-365-calendar-frontend

---------

Co-authored-by: Rajiv Sahal <rajivsahal@Rajivs-MacBook-Pro.local>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-06-06 15:08:38 +00:00
50eeb712e3 fix: Exclude location options for seated event (#15174)
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-06-06 13:24:00 +00:00
75edc4539c fix: [CAL-3578] [CAL-2733] Zoho calendar issues (#14905)
* fix zohocalender installation

* fix func name

* fix: [CAL-3578] [CAL-2733] check events in the calendar when checking availability

* fix: server location on credentials instead app keys

* refactor: remove console log

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

---------

Co-authored-by: pritam <pritam.pk36129@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-06-06 11:49:57 +00:00
Hariom Balhara def87ea84a fix: Team and User Links (#15342)
* fix: Team and User Links

* Add tests
2024-06-06 11:40:29 +00:00
Peer Richelsen 4153e5b2d5 added apple verification (#15346) 2024-06-06 11:04:26 +00:00
Omar López 8c2f6ae145 Revert "fix: 404 collisions (#15249)" (#15339)
This reverts commit 5d01eb2f38.

Signed-off-by: zomars <zomars@me.com>
2024-06-05 20:33:19 +00:00
78ea4d2f68 chore: Remove deprecated logo/avatar/away fields (#14705)
* chore: Remove deprecated logo/avatar/away fields

* Use right variables for accounts-step-card

* Remove more deprecated logo/avatar usage

* Dont drop the fields until all usages are gone

* Remove the avatar/logo drops for staggered release

* fix: Typescript blindspot (unvalidated include fields)

* fix: Further updates (mostly to API) typescript missed

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-06-05 16:44:31 +00:00
Hariom Balhara e7c638c1e4 fix: Wrong error message when inviting (#15332)
* fix: Wrong message when inviting

* Fix num users invited
2024-06-05 13:23:47 +00:00
Amit Sharma c28fab4fe2 fix: install flow: wrong org team avatars (#15334) 2024-06-05 13:10:31 +00:00
Alex van Andel e8a02554b6 chore: Remove DeploySentinel (#15328) 2024-06-05 00:18:11 +00:00
Kartik Saini 4017c0e28f feat: org-wide webhooks (#15144)
* feat: include org in teams list

* feat: org-wide webhooks

* Move getOrgIdFromMemberOrTeamId to packages/lib
2024-06-04 20:16:46 +00:00
Keith Williams d72f7f8a42 v4.1.9 (#15321)
release.yml / release (push) Canceled after 0s
2024-06-04 16:32:54 +00:00
3a4ebadbf3 feat: track no-shows in bookings (#15307)
* added dropdown for no-shows in past bookings

* added layout

* nit

* todo comment: figure out how to close dropdown

* more todo comments

* Adds TRPC route among other WIP

* functional update

* fixes type err

* fix type err

* fix: styles

* strikethrough in email

* NITs

* removed stale comment

* add tests

* err comment

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-06-04 15:18:11 +00:00
Hariom BalharaandJoe Au-Yeung 765d601ff9 fix: Booking Error - cannot be booked at this time (#15288)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-06-04 15:09:57 +00:00
3a22fdb428 feat: API v2 subversioning (#15135)
* feat: API v2 subversioning

* Update app.ts

* Added multiple versions to existing controllers

* Updated the slots controller

* chore: version platform library package with npm package alias

* chore: use consts to version

* chore: use consts to version

* chore: version api for cal provider

* chore: remove timezone controller subversion poc

* fixup! Merge branch 'main' into feat/api-v2-subversioning

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-06-04 15:05:39 +00:00
Rajiv SahalandRajiv Sahal 93580ac4bc fix: typo in Gcal atom prop and outlook service (#15319)
* fix typo in onCheckError prop

* pass in correct access token

---------

Co-authored-by: Rajiv Sahal <rajivsahal@Rajivs-MacBook-Pro.local>
2024-06-04 11:17:33 +00:00
Somay Chauhanandsean-brydon b15ad6f233 fix: inviting a team member dosent work when he login in via Google (#11312)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-06-04 07:54:26 +00:00
56e59ab3e1 fix: no rating submitted text fixed (#15007)
* no rating submitted text fixed

* Update packages/ui/components/empty-screen/EmptyScreen.tsx

* Update packages/ui/components/empty-screen/EmptyScreen.tsx

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-06-03 15:19:38 +00:00
Abhishek Gurav ee796933ee Updated CONTRIBUTING.md (#15102)
For AES256 CALENDSO_ENCRYPTION_KEY
2024-06-03 14:57:36 +00:00
Joe Au-Yeung d2f76edfd6 fix: Double encoding in addAppMutation (#15303)
* Remove double encoding

* Add test for OAuth page

* Add tests

* Remove test from integrations.e2e
2024-06-03 14:53:03 +00:00
Peer RichelsenandJoe Au-Yeung 822f0204a6 added admin API docs (#15297)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-06-03 14:41:13 +00:00
9770aff153 feat: outlook 365 calendar endpoints (#15077)
* init endpoints for office 365 syncing

* get web app url for oauth

* update typings

* nethods for office 365 calendar

* update office 365 connect endpoint

* update packages

* add calendar type and if office 365 calendar

* add SelectedCalendarsRepository and TokensModule

* add functions for getting oauth credentials, getting default calendars and redirect url for office 365 calendar

* update controllers

* e2e tests for office 365 calendar endpoints

* fixup! Merge branch 'main' into rajiv/cal-3544-outlookoffice365-integration-connect-atom

* fixup! fixup! Merge branch 'main' into rajiv/cal-3544-outlookoffice365-integration-connect-atom

* fix merge conflict

* update typings

* abstract microsoft outlook into its own module

* add one common interface for calendar apps

* make calendars dynamic

* abstract every calendar app into its own service

* cleanup

* make all calendar endpoints dynamic

* calendar app constants

* cleanup

* remove unused repositories

* rename microsoft calendar type

* fix typings

* add google and outlook calendar service to calendars module

* fixup

* some more e2e tests including  google calendar

* chore: small type change and apple calendar not implemented yet

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Rajiv Sahal <rajivsahal@Rajivs-MacBook-Pro.local>
2024-06-03 11:47:57 +00:00
Hichem Fantar 723d0ddec2 fix: Kbar component styles and improve transitions (#15283)
* fix: Kbar component styles

Fix Kbar styles on active state

* refactor: Add transition to event type list item

The commit refactors the event type list item component by adding a transition effect to improve the user experience.
2024-06-01 21:34:25 +00:00
Amit SharmaandSomay Chauhan 60b61eb625 feat: ability to assign people to round robin from org members (#14199)
* feat: ability to assign people to round robin from org members

* fix: include schedulingtype in filter

* fix: type check

* rename to EventTypesList

* rename to teams, change icon to link, early return if no events or teamId

* filter scheduling type array, change filter name

* rename to bulkAddMembersToTeams and bulkAddMembersToEventTypes

* filter refactor

* remove bulk

* filter remove null return

* Revert "filter remove null return"

This reverts commit 8ae6296dac24e5081b5896bb9dfe0c000c0f7faf.

* show checked if user already host in event type

* check boxes if user is part of the team

* remove hosts on unchecking boxes

* fix isSelected condition

* remove selected members from teams

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-06-01 21:30:52 +00:00
Omar López 5d2d99006b v4.1.8 (#15287)
release.yml / release (push) Canceled after 0s
2024-06-01 00:32:45 +00:00
Keith WilliamsandOmar López 5d01eb2f38 fix: 404 collisions (#15249)
* fix: 404 collisions

* Added E2E test

* Removed await

* Attempting to fix E2E

* fingers crossed

Signed-off-by: zomars <zomars@me.com>

* fix: e2e tests

* fix: missing navigation to 404 page

* fix: don't interrupt post login navigation

* fix: Expect owner as possible host for RR events

---------

Signed-off-by: zomars <zomars@me.com>
Co-authored-by: Omar López <zomars@me.com>
2024-05-31 22:28:43 +00:00
Joe Au-Yeung ac3fb0a1cd fix: Typo in deleteCredential handler (#15281)
* Fix typo

* Added optional chaining
2024-05-31 16:57:20 +00:00
Hariom Balhara 1730ef7d5d feat: Add framework to send all events fired for embed to Analytics Apps (#15173)
* Send all events to analytics apps

* feat: Start sending all events fired for embed to Analytics Apps(GTM Support added)

* Add tests
2024-05-31 16:07:09 +00:00
Somay Chauhan 9e371ded90 add redirectUrl to /eventTypes page as well (#15274)
* During signup, if the account already exists, we redirect the user to /event-types instead of onboarding. Adding this redirection logic to /event-types page as well to ensure the user is redirected to the correct redirectUrl.
2024-05-31 15:06:11 +00:00
Keith Williams 15098bdd1e chore: v4.1.7 (#15277)
release.yml / release (push) Canceled after 0s
2024-05-31 14:30:41 +00:00
625a7ec180 refactor: Introduce crmManager (#14272)
* fix timezone display on booking page to reflect event availability timezone

* migrate fetching event owner's schedule to server side

* migrate fetching event owner's schedule to server side

* fix e2e test errors

* Add WEBAPP_URL_FOR_OAUTH to salesforce auth

* In event manager constructor include "_crm" credentials as calendar creds

* Change crm apps to type to end with `_crm`

* Move sendgrid out of CRM

* Add zoho bigin to CRM apps

* When getting apps, use slug

* Add `crm` variants

* Hubspot Oauth use `WEBAPP_URL_FOR_OAUTH`

* Refactor creating credentials

* Fix empty CRM page

* Use credentials with `_crm`

* Abstract getAppCategoryTitle

* Add integration.handler changes

* Init crmManager

* Change salesforce to CrmService

* Create crmManager

* Create contact on new event

* Create event

* Create new CRM reference

* - Fix create new contact for salesforce
- Add reschedule to crmManager

* Create deleteAllCRMEvents

* When searching for credential, look for current credentials in class

* On cancel, delete 3rd party events

* Add delete method

* Type fix

* Type fix

* Convert Close.com to CrmService

* Convert Close.com to CrmService

* Move hubspot to CrmService

* Convert Pipedrive to CrmService

* Rename classes to CrmService

* Move ZohoCrm to CrmService

* Move Bigin to CrmService

* Type return for CrmServices

* Fix type errors

* Close.com create leads and contacts

* Fix tests

* Type fix

* Zoho bug fixes

* Clean up

* Type fixes

* Remove apiDeletes

* Type fixes

* Specific typing

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

* feat: Enable CRM apps on a per event type basis (#14450)

* Add Salesforce to be an event type app

* Handle new booking, only get enabled CRM credentials

* Abstract generating search params

* Add close.com to event type

* Clean up

* Move hubspot to event type

* Add pipedrive to event type

* Add zoho bigin to event type

* Add zoho crm to event type

* Remove console.log

* Add deleting CRM apps from event type

* Delete event type apps

* Fix deleting credentials

* Add CRM app data to event type metadata

* Backwards compatibility: add CRM credential if doesn't exist on event type

* Don't include user CRM credentials for backwards comp

* Backwards compatibility show CRM app is enabled and dirty field

* Clean up

* Type fixes

* Type fixes

* Type fix

* Type fix

* Remove console.log

* Test fix

* Upgrade embed-react vite version - dev

* Change build can't find error message

* Add back omni install prop

* Clean up

* Refactor `writeAppDataToEventType`

* Use eventType repository in writeAppDataToEventType

* Clean up old comments

* Add error logging

* createCRMEvents pass event uid as created event uid

* Use `getUid`

* Clean up props in create crm event

* Small changes to `crmManager`

* Fix zoho CRM

* refactor crmManager

* Undo vite config change

* Fix teamId query

* Fix bigin error

* Remove need for `writeAppDataToEventType`

* Add `getAllCredentials` test

* Add crmManager tests

* Type fixes

* Fix type errors

* Fix getAllCredentials test

* Fix tests

* Skip CRM manager tests for now

* feat: Skip RR Assignment if Contact Exists In Salesforce (#14556)

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

* Update yarn.lock

* @zomars feedback - use new URL for state params

* fix: update hook to not produce enabled === undefined

* fix: update app card interfaces to use the new enabled from useIsAppEnabled

* fix: feedback for crm RR skip (#15160)

* code clean up

* fix type any

* test setup for RR lead skip

* code clean up

* simplify code

* type error

* finshed first test for RR lead skip

* add seconds test

* add test for handleNewBooking

* test if teamMember is set

* fix missing enabled key

* fix tests

* fix type error

* use setSystemTime instead of getDate

* remove nested if

---------

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

* fix type error

* fix: remove app metadata from all eventTypes on deleting the app

* fix: update hook to not produce enabled === undefined (default to false)

---------

Co-authored-by: Shaik-Sirajuddin <sirajuddinshaik30gmail.com>
Co-authored-by: Shaik-Sirajuddin <sirajudddinshaik30@gmail.com>
Co-authored-by: Shaik-Sirajuddin <89742297+Shaik-Sirajuddin@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-05-31 13:54:52 +00:00
Omar López 5602f7bd95 chore: v4.1.6 (#15266)
release.yml / release (push) Canceled after 0s
2024-05-31 13:32:30 +00:00
Udit TakkarandKeith Williams 3e2caa9e1e fix: add blacklist email logic in backend (#15270)
* fix: add blacklist email logic in backedn

* chore: type err

* Update packages/features/bookings/lib/handleNewBooking.ts

* Update packages/features/bookings/lib/handleNewBooking.ts

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-05-31 12:47:36 +00:00
Lauris Skraucis b905de435d refactor: format v2 API managed user timeZone (#15273) 2024-05-31 12:06:29 +00:00
Amit Sharma 23bb06cc36 fix: Avatars not showing when using bookings filter (#15271) 2024-05-31 08:25:10 +00:00
Afrin NaharandKeith Williams dd24100035 fixes top right sections overflow on event page (#15109)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-05-31 04:23:19 +00:00
Vinicius VieiraandKeith Williams 7434d91d48 Adjust on getAggregateWorkingHours (#15137)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-05-31 04:01:11 +00:00
Keith WilliamsandUdit Takkar 3b1de34451 chore: Add guest email blacklist (#15255)
* chore: Add guest email blacklist

* types

* Added check to only log when we've removed one

* feat: add verification logic

* chore: use base exftract email

* Added toLowerCase for guest email checks

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-05-30 17:13:34 +00:00
4cf89d273e feat(i18n): complete Simplified Chinese translations (#14474)
* feat(i18n): complete Simplified Chinese translation

* chore: check for missing translation items

* fix: put the prevent merge conflict line at the end

* feat: put new translations at the beginning

* Rename translate-locales.ts to check-missing-translations.ts

* Update package.json to use check-missing-translations

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2024-05-30 15:06:58 +00:00
Dilpreet SinghandSio e58ce4af71 adds 45 min to dynamic events (#15248)
Co-authored-by: Sio <sio@Sios-MacBook-Air.local>
2024-05-30 14:01:45 +00:00
KaleemandAlex van Andel 15122dd5e9 fix: remove ts-ignore by converting to string[] (#14622)
* fix: remove ts-ignore by converting to string[]

* chore: remove extra space

* chore: make word lower case

* Possible fix for endpoints.ts

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-05-30 13:35:17 +00:00
sean-brydon 793c0c60e3 test: get upgradeable test (#15261)
* Add test for defaeult handler

* tests: add tests for getUsersTopbanner-getUpgradeable

* Import router to use userId
2024-05-30 12:28:40 +00:00
MorganandAlex van Andel 344454e574 chore: version platform libraries npm package (#15257)
* wip

* fix: watch mode and eslint slow

* update yarn lock

* Regenerated yarn.lock file for this branch

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-05-30 12:11:24 +00:00
sean-brydon 86342d103a Add test for defaeult handler (#15258) 2024-05-30 11:57:54 +00:00
Carina WollendorferandCarinaWolli d4f3726875 fix: missing date overrides in negative tz (#15251)
* fix check for past date overrides

* add test

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-05-29 23:35:43 +00:00
Hariom Balhara 817a3decb7 fix: Do subscription update even if team.members are less than 30 in case orgSeats allow it (#15209)
* Send all events to analytics apps

* fix: Handle subscription update when orgSeats are less than 30

* Add more tests
2024-05-29 21:19:32 +00:00
Keith Williams 5f082e11f8 fix: API booking issues (#15247)
* fix: API booking issues

* Put back the optional zod for user

* fixed the imports

* Using nullish

* typing errors

* missed one
2024-05-29 16:16:49 +00:00
Amit Sharma 5fb5081faa fix: removing the default app should remove the default badge from the appCard (#15243) 2024-05-29 15:10:45 +00:00
17f328fc5e chore: added help cards to platform dashboard (#15218)
* added cards to platform dashboard

* nit

* move help cards into its own component

* help cards component

* utils for platform

* fix icon type prop

* fix icon

---------

Co-authored-by: Rajiv Sahal <rajivsahal@Rajivs-MacBook-Pro.local>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2024-05-29 13:58:18 +00:00
Udit Takkar d19034c2d5 fix: double icon on user event type (#15239)
* fix: double icon

* test: add test for user event type
2024-05-29 13:23:58 +00:00
Morgan 4cdcc85620 fix: useGetBookingForReschedule wait for calProvider init (#15236) 2024-05-29 13:02:44 +00:00
Somay Chauhan 3f51ae626c openIntercom="true" open intercom (#15235) 2024-05-29 12:22:44 +00:00
Somay ChauhanandPeer Richelsen 8499aebe88 fix: intercom has organisation (#15224)
* fix: intercom has organisation

* Apply suggestions from code review

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-05-29 12:22:25 +00:00
Udit Takkar 07f613050e fix: not able to click start recording button (#15244) 2024-05-29 12:18:51 +00:00
Keith Williams 2c8c8368a0 chore: Reducing E2E test sharding (#15192)
* chore: Removing E2E test sharding

* Added back 4 shards for main E2E

* Bump build machines to use 8vCPUs

* Downgrading builds to 2 vCPUs to see time difference

* Reduced shards to 2 to see if flakes are reduced

* Increased E2E vCPUs

* Put builds back to 4 vCPUs since they need it

* Updated E2E suite to use 16 vCPUs and no sharding

* Not running in parallel just to test

* Put back the parallelism

* Get rid of themeBasis warning in test suite

* Skipping another flaky test

* 2 shards

* Update e2e.yml
2024-05-28 20:50:39 +00:00
Alex van Andel 74d3a84e27 v4.1.5 (#15230)
release.yml / release (push) Canceled after 0s
2024-05-28 20:15:53 +00:00
Somay Chauhan 33c1d49c52 feat: jelly seamless signup (#15220)
* feat: jelly seamless signup

* fix: wrong import

* add credentialId id to defaultLocation

* add CredentialId and correct appType

* fix: type error
2024-05-28 17:54:59 +00:00
889ff177c1 feat: Add {first name} variable to Organizer & Scheduler custom event… (#14156)
* feat: Add {first name} variable to Organizer & Scheduler custom event name

* fix: use split name booking field

* remove unwanted translation changes

* Update packages/core/event.ts

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>

* chore: improve code

* add scheduler last name

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-05-28 14:32:12 +00:00
Morgan d8a6c375d0 chore: Platform Booker defaultFormValues for prefilling (#15219)
* chore: Platform Booker defaultFormValues for prefilling

* fixup! chore: Platform Booker defaultFormValues for prefilling
2024-05-28 11:36:54 +00:00
Lauris Skraucis 655127d155 fix: exclude atoms web wrappers from build (#15216)
* fix: exclude atoms web wrappers from build

* refactor: tsconfig web wrapper regex

* refactor: wrappers follow schema Component(Web|Platform)Wrapper.tsx

* fix: tsconfig regex for web wrapper
2024-05-28 10:09:03 +00:00
Morgan 17c4a10e33 fix: platform plan name is always uppercase (#15215) 2024-05-28 08:17:42 +00:00
Udit Takkar 30f9f19a9f fix: avatar on booking page (#15190)
* fix: avatar on booking page

* chore

* fix: type err

* fix: unpublished org
2024-05-27 19:31:08 +00:00
Anik Dhabal BabuandPeer Richelsen be7d45afe2 feat: autosave availability after making changes (#14849)
* feat: availability after making changes

* update

* update

* Update Schedule.tsx

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-05-27 14:25:29 +00:00
Amit SharmaandUdit Takkar d2bec46d89 fix: event duration missing translation (#14862)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-05-27 14:12:00 +00:00
Sujit Ale MagarandUdit Takkar b90500778e fix: Event Type Toggle Shift (#15066)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-05-27 14:10:53 +00:00
Rajiv SahalandMorgan Vernay bddc235c3c fix: platform booker icons and address property for type inPerson (#15207)
* fix: platform booker icons

* fix label not being shown for address

* fix type check

* PR feedback

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-05-27 11:02:58 +00:00
360b729d08 chore: added event-types link to settings (#14274)
* added event-types link

* fix: add item in settings layout

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-05-27 09:35:52 +00:00
Alex van Andel 5d325495a9 fix: Profile page crash due to invalid topLevelElement (#15198) 2024-05-24 16:12:15 +00:00
Amit Sharma dead2b843a fix: attendeeSeatId is not being returned on webhook response for events with seats (#15184) 2024-05-24 15:47:35 +00:00
Olga Stogova 1174cb0d8e fix(atoms): fix max-height bug for the Booker component (#15195) 2024-05-24 13:15:04 +00:00
Olga Stogova 58e6f18594 fix: styles for the schedule component, make it mobile-friendly (#15178)
* fix styles for the schedule component, make it mobile-friendly

* CR changes

* no need that condition
2024-05-24 11:06:40 +00:00
Udit Takkar ffe6a46e66 chore: return empty array if no transcription found (#15185) 2024-05-24 02:20:50 +00:00
673 changed files with 22180 additions and 4106 deletions
+3
View File
@@ -359,3 +359,6 @@ UNKEY_ROOT_KEY=
# Used for Cal.ai Enterprise Voice AI Agents
# https://retellai.com
RETELL_AI_KEY=
# Used to disallow emails as being added as guests on bookings
BLACKLISTED_GUEST_EMAILS=
+2
View File
@@ -1,4 +1,6 @@
node_modules
apps/api/v2/dist
packages/platform/**/dist/*
**/**/node_modules
**/**/.next
**/**/public
+2 -1
View File
@@ -20,13 +20,14 @@ runs:
key-2: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', '!**/node_modules') }}
key-3: ${{ github.event.pull_request.number || github.ref }}
key-4: ${{ github.sha }}
key-5: ${{ github.event.pull_request.head.sha }}
with:
path: |
${{ github.workspace }}/apps/web/.next
${{ github.workspace }}/apps/web/public/embed
**/.turbo/**
**/dist/**
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}-${{ env.key-5 }}
- run: |
export NODE_OPTIONS="--max_old_space_size=8192"
yarn build
@@ -65,7 +65,7 @@ jobs:
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/cache-db
- name: Cache API v1 production build
uses: buildjet/cache@v3
uses: buildjet/cache@v4
id: cache-api-v1-build
env:
cache-name: api-v1-build
@@ -34,7 +34,7 @@ jobs:
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- name: Cache API v2 production build
uses: buildjet/cache@v3
uses: buildjet/cache@v4
id: cache-api-v2-build
env:
cache-name: api-v2-build
+3 -6
View File
@@ -41,7 +41,7 @@ env:
jobs:
e2e-app-store:
timeout-minutes: 20
name: E2E App Store (${{ matrix.shard }}/${{ strategy.job-total }})
name: E2E App Store
runs-on: buildjet-4vcpu-ubuntu-2204
services:
postgres:
@@ -70,9 +70,6 @@ jobs:
- 1025:1025
strategy:
fail-fast: false
matrix:
## There aren't many tests for AppStore. So, just start with 2 shards. Increase if needed.
shard: [1, 2]
steps:
- uses: docker/login-action@v3
@@ -93,10 +90,10 @@ jobs:
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }}
run: yarn e2e:app-store
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }}
name: app-store-results
path: test-results
+3 -6
View File
@@ -41,7 +41,7 @@ env:
jobs:
e2e-embed:
timeout-minutes: 20
name: E2E Embed React (${{ matrix.shard }}/${{ strategy.job-total }})
name: E2E Embed React
runs-on: buildjet-4vcpu-ubuntu-2204
services:
postgres:
@@ -62,9 +62,6 @@ jobs:
- 5432:5432
strategy:
fail-fast: false
matrix:
## There aren't many tests for embed-react. So, just start with 2 shards. Increase if needed.
shard: [1, 2]
steps:
- uses: docker/login-action@v3
@@ -79,11 +76,11 @@ jobs:
- uses: ./.github/actions/cache-build
- name: Run Tests
run: |
yarn e2e:embed-react --shard=${{ matrix.shard }}/${{ strategy.job-total }}
yarn e2e:embed-react
yarn workspace @calcom/embed-react packaged:tests
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }}
name: embed-react-results
path: test-results
+3 -6
View File
@@ -41,7 +41,7 @@ env:
jobs:
e2e-embed:
timeout-minutes: 20
name: E2E Embed Core (${{ matrix.shard }}/${{ strategy.job-total }})
name: E2E Embed Core
runs-on: buildjet-4vcpu-ubuntu-2204
services:
postgres:
@@ -70,9 +70,6 @@ jobs:
- 1025:1025
strategy:
fail-fast: false
matrix:
## There aren't many tests for embed-core. So, just start with 2 shards. Increase if needed.
shard: [1, 2]
steps:
- uses: docker/login-action@v3
@@ -86,10 +83,10 @@ jobs:
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn e2e:embed --shard=${{ matrix.shard }}/${{ strategy.job-total }}
run: yarn e2e:embed
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }}
name: embed-core-results
path: test-results
+2 -2
View File
@@ -42,7 +42,7 @@ jobs:
e2e:
timeout-minutes: 20
name: E2E (${{ matrix.shard }}/${{ strategy.job-total }})
runs-on: buildjet-4vcpu-ubuntu-2204
runs-on: buildjet-8vcpu-ubuntu-2204
services:
postgres:
image: postgres:13
@@ -71,7 +71,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7]
shard: [1, 2, 3, 4]
steps:
- uses: docker/login-action@v3
with:
@@ -45,13 +45,7 @@ env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
build:
name: Production builds
if: ${{ github.event_name == 'push' }}
uses: ./.github/workflows/production-build-without-database.yml
secrets: inherit
analyze:
needs: build
if: always()
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
+54
View File
@@ -0,0 +1,54 @@
name: PR Labeled with ready-for-e2e
on:
pull_request_target:
types: [labeled]
branches:
- main
- gh-actions-test-branch
workflow_dispatch:
jobs:
run-e2e-jobs:
name: Run E2E Jobs
runs-on: buildjet-2vcpu-ubuntu-2204
permissions:
pull-requests: read
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- name: Check Label and Retrigger Checks
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const labelName = context.payload.label.name;
const prNumber = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
if (labelName === 'ready-for-e2e') {
console.log(`Running E2E jobs for PR #${prNumber}...`);
const checkRuns = await github.rest.checks.listForRef({
owner,
repo,
ref: context.payload.pull_request.head.sha,
});
for (const check of checkRuns.data.check_runs) {
console.log('check.name', check.name);
if (check.name.includes('e2e')) {
await github.rest.actions.reRunJob({
owner,
repo,
job_id: check.id,
});
console.log(`Triggering job #${check.id}`);
}
}
console.log(`Triggered E2E checks for PR #${prNumber}`);
} else {
console.log(`Label ${labelName} does not trigger re-running checks.`);
}
+17
View File
@@ -0,0 +1,17 @@
name: PR Reviewed
on:
pull_request_review:
types: [submitted]
jobs:
label-pr:
runs-on: ubuntu-latest
steps:
- name: Label PR as ready for E2E
if: github.event.review.state == 'approved'
uses: actions-ecosystem/action-add-labels@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
labels: 'ready-for-e2e'
+117 -6
View File
@@ -4,6 +4,7 @@ on:
pull_request_target:
branches:
- main
- gh-actions-test-branch
workflow_dispatch:
concurrency:
@@ -18,6 +19,7 @@ jobs:
pull-requests: read
outputs:
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
commit-sha: ${{ steps.get_sha.outputs.commit-sha }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
@@ -27,37 +29,146 @@ jobs:
filters: |
has-files-requiring-all-checks:
- "!(**.md|.github/CODEOWNERS)"
- name: Get Latest Commit SHA
id: get_sha
run: |
echo "commit-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
check-label:
needs: [changes]
runs-on: buildjet-2vcpu-ubuntu-2204
name: Check for E2E label
outputs:
run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' && (github.event.action != 'labeled' || (github.event.action == 'labeled' && github.event.label.name == 'ready-for-e2e')) }}
steps:
- name: Check if PR exists with ready-for-e2e label for this SHA
id: check-if-pr-has-label
uses: actions/github-script@v7
with:
script: |
let labels = [];
if (context.payload.pull_request) {
labels = context.payload.pull_request.labels;
} else {
try {
const sha = '${{ needs.changes.outputs.commit-sha }}';
console.log('sha', sha);
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: sha
});
if (prs.length === 0) {
core.setOutput('run-e2e', false);
console.log(`No pull requests found for commit SHA ${sha}`);
return;
}
const pr = prs[0];
console.log(`PR number: ${pr.number}`);
console.log(`PR title: ${pr.title}`);
console.log(`PR state: ${pr.state}`);
console.log(`PR URL: ${pr.html_url}`);
labels = pr.labels;
}
catch (e) {
core.setOutput('run-e2e', false);
console.log(e);
}
}
const labelFound = labels.map(l => l.name).includes('ready-for-e2e');
console.log('Found the label?', labelFound);
core.setOutput('run-e2e', labelFound);
type-check:
name: Type check
needs: [changes]
needs: [changes, check-label]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/check-types.yml
secrets: inherit
lint:
name: Linters
needs: [changes]
needs: [changes, check-label]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/lint.yml
secrets: inherit
unit-test:
name: Tests
needs: [changes]
needs: [changes, check-label]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/unit-tests.yml
secrets: inherit
integration-test:
name: Tests
needs: [changes]
needs: [changes, check-label]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/integration-tests.yml
secrets: inherit
build-api-v1:
name: Production builds
needs: [changes, check-label]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/api-v1-production-build.yml
secrets: inherit
build-api-v2:
name: Production builds
needs: [changes, check-label]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/api-v2-production-build.yml
secrets: inherit
build:
name: Production builds
needs: [changes, check-label]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/production-build-without-database.yml
secrets: inherit
e2e:
name: e2e
needs: [changes, check-label, build]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e.yml
secrets: inherit
e2e-app-store:
name: e2e-app-store
needs: [changes, check-label, build]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-app-store.yml
secrets: inherit
e2e-embed:
name: e2e-embed
needs: [changes, check-label, build]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-embed.yml
secrets: inherit
e2e-embed-react:
name: e2e-embed-react
needs: [changes, check-label, build]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-embed-react.yml
secrets: inherit
analyze:
name: Analyze Build
needs: [build]
uses: ./.github/workflows/nextjs-bundle-analysis.yml
secrets: inherit
required:
needs: [changes, lint, type-check, unit-test, integration-test]
if: always()
needs: [changes, lint, type-check, unit-test, integration-test, check-label, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
- name: fail if conditional jobs failed
@@ -37,12 +37,3 @@ jobs:
```
${{ steps.lint_pr_title.outputs.error_message }}
```
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
message: |
Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link.
+1 -1
View File
@@ -99,7 +99,7 @@ To develop locally:
- Duplicate `.env.example` to `.env`.
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
- Use `openssl rand -base64 32` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
6. Setup Node
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
+15
View File
@@ -0,0 +1,15 @@
import * as Sentry from "@sentry/nextjs";
export function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
});
}
if (process.env.NEXT_RUNTIME === "edge") {
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
});
}
}
@@ -0,0 +1,90 @@
import type { Request, Response } from "express";
import type { NextApiResponse, NextApiRequest } from "next";
import { createMocks } from "node-mocks-http";
import { describe, it, expect, vi } from "vitest";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { rateLimitApiKey } from "~/lib/helpers/rateLimitApiKey";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({
checkRateLimitAndThrowError: vi.fn(),
}));
describe("rateLimitApiKey middleware", () => {
it("should return 401 if no apiKey is provided", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {},
});
await rateLimitApiKey(req, res, vi.fn() as any);
expect(res._getStatusCode()).toBe(401);
expect(res._getJSONData()).toEqual({ message: "No apiKey provided" });
});
it("should call checkRateLimitAndThrowError with correct parameters", async () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
});
(checkRateLimitAndThrowError as any).mockResolvedValueOnce({
limit: 100,
remaining: 99,
reset: Date.now(),
});
// @ts-expect-error weird typing between middleware and createMocks
await rateLimitApiKey(req, res, vi.fn() as any);
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
identifier: "test-key",
rateLimitingType: "api",
onRateLimiterResponse: expect.any(Function),
});
});
it("should set rate limit headers correctly", async () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
});
const rateLimiterResponse = {
limit: 100,
remaining: 99,
reset: Date.now(),
};
(checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => {
onRateLimiterResponse(rateLimiterResponse);
});
// @ts-expect-error weird typing between middleware and createMocks
await rateLimitApiKey(req, res, vi.fn() as any);
expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit);
expect(res.getHeader("X-RateLimit-Remaining")).toBe(rateLimiterResponse.remaining);
expect(res.getHeader("X-RateLimit-Reset")).toBe(rateLimiterResponse.reset);
});
it("should return 429 if rate limit is exceeded", async () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
});
(checkRateLimitAndThrowError as any).mockRejectedValue(new Error("Rate limit exceeded"));
// @ts-expect-error weird typing between middleware and createMocks
await rateLimitApiKey(req, res, vi.fn() as any);
expect(res._getStatusCode()).toBe(429);
expect(res._getJSONData()).toEqual({ message: "Rate limit exceeded" });
});
});
+13 -9
View File
@@ -6,15 +6,19 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
// TODO: Add a way to add trusted api keys
await checkRateLimitAndThrowError({
identifier: req.query.apiKey as string,
rateLimitingType: "api",
onRateLimiterResponse: (response) => {
res.setHeader("X-RateLimit-Limit", response.limit);
res.setHeader("X-RateLimit-Remaining", response.remaining);
res.setHeader("X-RateLimit-Reset", response.reset);
},
});
try {
await checkRateLimitAndThrowError({
identifier: req.query.apiKey as string,
rateLimitingType: "api",
onRateLimiterResponse: (response) => {
res.setHeader("X-RateLimit-Limit", response.limit);
res.setHeader("X-RateLimit-Remaining", response.remaining);
res.setHeader("X-RateLimit-Reset", response.reset);
},
});
} catch (error) {
res.status(429).json({ message: "Rate limit exceeded" });
}
await next();
};
+1 -1
View File
@@ -55,7 +55,7 @@ export const schemaBookingReadPublic = Booking.extend({
timeZone: true,
locale: true,
})
.optional(),
.nullish(),
payment: z
.array(
_PaymentModel.pick({
@@ -14,3 +14,7 @@ export const withValidQueryIdTransformParseInt = withValidation({
type: "Zod",
mode: "query",
});
export const getTranscriptFromRecordingId = schemaQueryIdParseInt.extend({
recordingId: z.string(),
});
+11 -6
View File
@@ -2,7 +2,12 @@ const { withAxiom } = require("next-axiom");
const { withSentryConfig } = require("@sentry/nextjs");
const plugins = [withAxiom];
/** @type {import("next").NextConfig} */
const nextConfig = {
experimental: {
instrumentationHook: true,
},
transpilePackages: [
"@calcom/app-store",
"@calcom/core",
@@ -87,12 +92,12 @@ const nextConfig = {
};
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
nextConfig["sentry"] = {
autoInstrumentServerFunctions: true,
hideSourceMaps: true,
};
plugins.push(withSentryConfig);
plugins.push((nextConfig) =>
withSentryConfig(nextConfig, {
autoInstrumentServerFunctions: true,
hideSourceMaps: true,
})
);
}
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
+1 -1
View File
@@ -30,7 +30,7 @@
"@calcom/lib": "*",
"@calcom/prisma": "*",
"@calcom/trpc": "*",
"@sentry/nextjs": "^7.73.0",
"@sentry/nextjs": "^8.8.0",
"bcryptjs": "^2.4.3",
"memory-cache": "^0.2.0",
"next": "^13.5.4",
@@ -0,0 +1,94 @@
import type { NextApiRequest } from "next";
import {
getTranscriptsAccessLinkFromRecordingId,
checkIfRoomNameMatchesInRecording,
} from "@calcom/core/videoClient";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import type { PartialReference } from "@calcom/types/EventManager";
import { getTranscriptFromRecordingId } from "~/lib/validations/shared/queryIdTransformParseInt";
/**
* @swagger
* /bookings/{id}/transcripts/{recordingId}:
* get:
* summary: Find all Cal video transcripts of that recording
* operationId: getTranscriptsByRecordingId
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID of the booking for which transcripts need to be fetched.
* - in: path
* name: recordingId
* schema:
* type: string
* required: true
* description: ID of the recording(daily.co recording id) for which transcripts need to be fetched.
* - in: query
* name: apiKey
* required: true
* schema:
* type: string
* description: Your API key
* tags:
* - bookings
* responses:
* 200:
* description: OK
* content:
* application/json:
* 401:
* description: Authorization information is missing or invalid.
* 404:
* description: Booking was not found
*/
export async function getHandler(req: NextApiRequest) {
const { query } = req;
const { id, recordingId } = getTranscriptFromRecordingId.parse(query);
await checkIfRecordingBelongsToBooking(id, recordingId);
const transcriptsAccessLinks = await getTranscriptsAccessLinkFromRecordingId(recordingId);
return transcriptsAccessLinks;
}
const checkIfRecordingBelongsToBooking = async (bookingId: number, recordingId: string) => {
const booking = await prisma.booking.findUnique({
where: { id: bookingId },
include: { references: true },
});
if (!booking)
throw new HttpError({
statusCode: 404,
message: `No Booking found with booking id ${bookingId}`,
});
const roomName =
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
undefined;
if (!roomName)
throw new HttpError({
statusCode: 404,
message: `No Booking Reference with Daily Video found with booking id ${bookingId}`,
});
const canUserAccessRecordingId = await checkIfRoomNameMatchesInRecording(roomName, recordingId);
if (!canUserAccessRecordingId) {
throw new HttpError({
statusCode: 403,
message: `This Recording Id ${recordingId} does not belong to booking ${bookingId}`,
});
}
};
export default defaultResponder(getHandler);
@@ -0,0 +1,16 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import { withMiddleware } from "~/lib/helpers/withMiddleware";
import authMiddleware from "../../_auth-middleware";
export default withMiddleware()(
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
await authMiddleware(req);
return defaultHandler({
GET: import("./_get"),
})(req, res);
})
);
+12 -1
View File
@@ -2,6 +2,8 @@ import type { NextApiRequest } from "next";
import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi";
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -218,7 +220,16 @@ async function handler(req: NextApiRequest) {
req.userId = requestedUserId || userId;
}
return await handleNewBooking(req, getBookingDataSchemaForApi);
try {
return await handleNewBooking(req, getBookingDataSchemaForApi);
} catch (error: unknown) {
const knownError = error as Error;
if (knownError?.message === ErrorCode.NoAvailableUsersFound) {
throw new HttpError({ statusCode: 400, message: knownError.message });
}
throw error;
}
}
export default defaultResponder(handler);
+6 -1
View File
@@ -7,7 +7,12 @@ import { schemaUserReadPublic } from "~/lib/validations/user";
async function handler({ userId }: NextApiRequest) {
const data = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
return { user: schemaUserReadPublic.parse(data) };
return {
user: schemaUserReadPublic.parse({
...data,
avatar: data.avatarUrl,
}),
};
}
export default defaultResponder(handler);
+4 -1
View File
@@ -45,7 +45,10 @@ export async function getHandler(req: NextApiRequest) {
if (!isSystemWideAdmin && query.userId !== req.userId)
throw new HttpError({ statusCode: 403, message: "Forbidden" });
const data = await prisma.user.findUnique({ where: { id: query.userId } });
const user = schemaUserReadPublic.parse(data);
const user = schemaUserReadPublic.parse({
...data,
avatar: data?.avatarUrl,
});
return { user };
}
+12 -1
View File
@@ -2,7 +2,9 @@ import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { uploadAvatar } from "@calcom/lib/server/avatar";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId";
import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validations/user";
@@ -101,7 +103,8 @@ export async function patchHandler(req: NextApiRequest) {
if (!isSystemWideAdmin && query.userId !== req.userId)
throw new HttpError({ statusCode: 403, message: "Forbidden" });
const body = await schemaUserEditBodyParams.parseAsync(req.body);
const { avatar, ...body }: { avatar?: string | undefined } & Prisma.UserUpdateInput =
await schemaUserEditBodyParams.parseAsync(req.body);
// disable role or branding changes unless admin.
if (!isSystemWideAdmin) {
if (body.role) body.role = undefined;
@@ -119,6 +122,14 @@ export async function patchHandler(req: NextApiRequest) {
message: "Bad request: Invalid default schedule id",
});
}
if (avatar) {
body.avatarUrl = await uploadAvatar({
userId: query.userId,
avatar: await (await import("@calcom/lib/server/resizeBase64Image")).resizeBase64Image(avatar),
});
}
const data = await prisma.user.update({
where: { id: query.userId },
data: body,
-5
View File
@@ -1,5 +0,0 @@
import { init as SentryInit } from "@sentry/nextjs";
SentryInit({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
});
-6
View File
@@ -1,6 +0,0 @@
import { init as SentryInit } from "@sentry/nextjs";
SentryInit({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
});
@@ -0,0 +1,129 @@
import prismaMock from "../../../../../../../../../tests/libs/__mocks__/prismaMock";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test, vi, afterEach } from "vitest";
import {
getTranscriptsAccessLinkFromRecordingId,
checkIfRoomNameMatchesInRecording,
} from "@calcom/core/videoClient";
import { buildBooking } from "@calcom/lib/test/builder";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
import authMiddleware from "../../../../../../pages/api/bookings/[id]/_auth-middleware";
import handler from "../../../../../../pages/api/bookings/[id]/transcripts/[recordingId]/_get";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
vi.mock("@calcom/core/videoClient", () => {
return {
getTranscriptsAccessLinkFromRecordingId: vi.fn(),
checkIfRoomNameMatchesInRecording: vi.fn(),
};
});
vi.mock("~/lib/utils/retrieveScopedAccessibleUsers", () => {
return {
getAccessibleUsers: vi.fn(),
};
});
afterEach(() => {
vi.resetAllMocks();
});
const mockGetTranscripts = () => {
const downloadLinks = [{ format: "json", link: "https://URL1" }];
vi.mocked(getTranscriptsAccessLinkFromRecordingId).mockResolvedValue(downloadLinks);
vi.mocked(checkIfRoomNameMatchesInRecording).mockResolvedValue(true);
return downloadLinks;
};
const recordingId = "abc-xyz";
describe("GET /api/bookings/[id]/transcripts/[recordingId]", () => {
test("Returns transcripts if user is system-wide admin", async () => {
const adminUserId = 1;
const userId = 2;
const bookingId = 1111;
prismaMock.booking.findUnique.mockResolvedValue(
buildBooking({
id: bookingId,
userId,
references: [
{
id: 1,
type: "daily_video",
uid: "17OHkCH53pBa03FhxMbw",
meetingId: "17OHkCH53pBa03FhxMbw",
meetingPassword: "password",
meetingUrl: "https://URL",
},
],
})
);
const mockedTranscripts = mockGetTranscripts();
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: bookingId,
recordingId,
},
});
req.isSystemWideAdmin = true;
req.userId = adminUserId;
await authMiddleware(req);
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(JSON.parse(res._getData())).toEqual(mockedTranscripts);
});
test("Allows GET transcripts when user is org-wide admin", async () => {
const adminUserId = 1;
const memberUserId = 10;
const bookingId = 3333;
prismaMock.booking.findUnique.mockResolvedValue(
buildBooking({
id: bookingId,
userId: memberUserId,
references: [
{ id: 1, type: "daily_video", uid: "17OHkCH53pBa03FhxMbw", meetingId: "17OHkCH53pBa03FhxMbw" },
],
})
);
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: bookingId,
recordingId,
},
});
req.userId = adminUserId;
req.isOrganizationOwnerOrAdmin = true;
mockGetTranscripts();
vi.mocked(getAccessibleUsers).mockResolvedValue([memberUserId]);
await authMiddleware(req);
await handler(req, res);
expect(res.statusCode).toBe(200);
});
});
+4
View File
@@ -20,3 +20,7 @@ STRIPE_API_KEY=
STRIPE_WEBHOOK_SECRET=
WEB_APP_URL=http://localhost:3000/
CALCOM_LICENSE_KEY=
API_KEY_PREFIX=cal_
GET_LICENSE_KEY_URL="https://console.cal.com/api/license"
IS_E2E=false
+1
View File
@@ -2,5 +2,6 @@ const rootConfig = require("../../../packages/config/prettier-preset");
module.exports = {
...rootConfig,
importOrder: ["^./instrument", ...rootConfig.importOrder],
importOrderParserPlugins: ["typescript", "decorators-legacy"],
};
+10
View File
@@ -42,6 +42,16 @@ $ yarn prisma generate
Copy `.env.example` to `.env` and fill values.
## Add license Key to deployments table in DB
id, logo theme licenseKey agreedLicenseAt
1, null, null, 'c4234812-12ab-42s6-a1e3-55bedd4a5bb7', '2023-05-15 21:39:47.611'
your CALCOM_LICENSE_KEY env var need to contain the same value
.env
CALCOM_LICENSE_KEY=c4234812-12ab-42s6-a1e3-55bedd4a5bb
## Running the app
```bash
+8 -5
View File
@@ -9,8 +9,8 @@
"build": "yarn dev:build && nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"dev:build:watch": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch & yarn workspace @calcom/platform-libraries build:watch",
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build",
"dev:build:watch": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch",
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build",
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && yarn start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node ./dist/apps/api/v2/src/main.js",
@@ -25,11 +25,14 @@
},
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-libraries": "*",
"@calcom/platform-libraries-0.0.13": "npm:@calcom/platform-libraries@0.0.13",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-libraries-0.0.4": "npm:@calcom/platform-libraries@0.0.4",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
"@golevelup/ts-jest": "^0.4.0",
"@microsoft/microsoft-graph-types-beta": "^0.42.0-preview",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
@@ -38,8 +41,7 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.3.0",
"@nestjs/throttler": "^5.1.2",
"@sentry/node": "^7.86.0",
"@sentry/tracing": "^7.86.0",
"@sentry/node": "^8.8.0",
"body-parser": "^1.20.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
@@ -55,6 +57,7 @@
"next-auth": "^4.22.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"querystring": "^0.2.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"stripe": "^15.3.0",
-2
View File
@@ -50,8 +50,6 @@ import { AppController } from "./app.controller";
EndpointsModule,
AuthModule,
JwtModule,
//register prefix for all routes in EndpointsModule
RouterModule.register([{ path: "/v2", module: EndpointsModule }]),
],
controllers: [AppController],
providers: [
+34 -15
View File
@@ -1,26 +1,41 @@
import { getEnv } from "@/env";
import "./instrument";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { SentryFilter } from "@/filters/sentry-exception.filter";
import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
import type { ValidationError } from "@nestjs/common";
import { BadRequestException, ValidationPipe, VersioningType } from "@nestjs/common";
import { HttpAdapterHost } from "@nestjs/core";
import { BaseExceptionFilter, HttpAdapterHost } from "@nestjs/core";
import type { NestExpressApplication } from "@nestjs/platform-express";
import * as Sentry from "@sentry/node";
import * as cookieParser from "cookie-parser";
import { Request } from "express";
import helmet from "helmet";
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import {
API_VERSIONS,
VERSION_2024_04_15,
API_VERSIONS_ENUM,
CAL_API_VERSION_HEADER,
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
} from "@calcom/platform-constants";
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
app.enableShutdownHooks();
app.enableVersioning({
type: VersioningType.URI,
prefix: "v",
defaultVersion: "1",
type: VersioningType.CUSTOM,
extractor: (request: unknown) => {
const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined;
if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) {
return headerVersion;
}
return VERSION_2024_04_15;
},
defaultVersion: VERSION_2024_04_15,
});
app.use(helmet());
@@ -28,7 +43,15 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
app.enableCors({
origin: "*",
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
allowedHeaders: [X_CAL_CLIENT_ID, X_CAL_SECRET_KEY, "Accept", "Authorization", "Content-Type", "Origin"],
allowedHeaders: [
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
CAL_API_VERSION_HEADER,
"Accept",
"Authorization",
"Content-Type",
"Origin",
],
maxAge: 86_400,
});
@@ -46,15 +69,11 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
})
);
if (process.env.SENTRY_DSN) {
Sentry.init({
dsn: getEnv("SENTRY_DSN"),
});
}
// Exception filters, new filters go at the bottom, keep the order
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new SentryFilter(httpAdapter));
if (process.env.SENTRY_DSN) {
Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter));
}
app.useGlobalFilters(new PrismaExceptionFilter());
app.useGlobalFilters(new ZodExceptionFilter());
app.useGlobalFilters(new HttpExceptionFilter());
+4
View File
@@ -15,6 +15,9 @@ const loadConfig = (): AppConfig => {
? `:${Number(getEnv("API_PORT", "5555"))}`
: ""
}/v2`,
keyPrefix: getEnv("API_KEY_PREFIX", "cal_"),
licenseKey: getEnv("CALCOM_LICENSE_KEY", ""),
licenseKeyUrl: getEnv("GET_LICENSE_KEY_URL", "https://console.cal.com/api/license"),
},
db: {
readUrl: getEnv("DATABASE_READ_URL"),
@@ -31,6 +34,7 @@ const loadConfig = (): AppConfig => {
app: {
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
},
e2e: getEnv("IS_E2E", false),
};
};
+4
View File
@@ -6,6 +6,9 @@ export type AppConfig = {
port: number;
path: string;
url: string;
keyPrefix: string;
licenseKey: string;
licenseKeyUrl: string;
};
db: {
readUrl: string;
@@ -22,4 +25,5 @@ export type AppConfig = {
app: {
baseUrl: string;
};
e2e: boolean;
};
@@ -2,11 +2,10 @@ import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
import { SchedulesModule } from "@/ee/schedules/schedules.module";
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
@@ -17,10 +16,10 @@ import * as request from "supertest";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { handleNewBooking } from "@calcom/platform-libraries";
import { handleNewBooking } from "@calcom/platform-libraries-0.0.2";
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
describe("Bookings Endpoints", () => {
@@ -29,7 +28,7 @@ describe("Bookings Endpoints", () => {
let userRepositoryFixture: UserRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService;
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
const userEmail = "bookings-controller-e2e@api.com";
@@ -40,10 +39,10 @@ describe("Bookings Endpoints", () => {
let createdBooking: Awaited<ReturnType<typeof handleNewBooking>>;
beforeAll(async () => {
const moduleRef = await withAccessTokenAuth(
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule, SchedulesModule],
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
)
.overrideGuard(PermissionsGuard)
@@ -55,13 +54,13 @@ describe("Bookings Endpoints", () => {
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService>(SchedulesService);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
user = await userRepositoryFixture.create({
email: userEmail,
});
const userSchedule: CreateScheduleInput = {
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
@@ -2,9 +2,10 @@ import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input";
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { BillingService } from "@/modules/billing/services/billing.service";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
@@ -37,14 +38,14 @@ import {
getBookingInfo,
handleCancelBooking,
getBookingForReschedule,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries-0.0.2";
import {
handleNewBooking,
BookingResponse,
HttpError,
handleNewRecurringBooking,
handleInstantMeeting,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries-0.0.2";
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
import { ApiResponse } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
@@ -72,8 +73,8 @@ const DEFAULT_PLATFORM_PARAMS = {
};
@Controller({
path: "/bookings",
version: "2",
path: "/v2/bookings",
version: API_VERSIONS_VALUES,
})
@UseGuards(PermissionsGuard)
@DocsTags("Bookings")
@@ -88,7 +89,7 @@ export class BookingsController {
) {}
@Get("/")
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
@Permissions([BOOKING_READ])
@ApiQuery({ name: "filters[status]", enum: Status, required: true })
@ApiQuery({ name: "limit", type: "number", required: false })
@@ -1,7 +1,7 @@
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
import type { AppsStatus } from "@calcom/platform-libraries";
import type { AppsStatus } from "@calcom/platform-libraries-0.0.2";
export class CreateRecurringBookingInput extends CreateBookingInput {
@IsBoolean()
@@ -0,0 +1,211 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User, Credential } from "@prisma/client";
import * as request from "supertest";
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import {
GOOGLE_CALENDAR,
OFFICE_365_CALENDAR,
GOOGLE_CALENDAR_TYPE,
GOOGLE_CALENDAR_ID,
} from "@calcom/platform-constants";
import { OFFICE_365_CALENDAR_ID, OFFICE_365_CALENDAR_TYPE } from "@calcom/platform-constants";
const CLIENT_REDIRECT_URI = "http://localhost:5555";
class CalendarsServiceMock {
async getCalendars() {
return {
connectedCalendars: [
{
integration: {
type: "google_calendar",
},
},
],
};
}
}
describe("Platform Calendars Endpoints", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let tokensRepositoryFixture: TokensRepositoryFixture;
let credentialsRepositoryFixture: CredentialsRepositoryFixture;
let user: User;
let office365Credentials: Credential;
let googleCalendarCredentials: Credential;
let accessTokenSecret: string;
let refreshTokenSecret: string;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, TokensModule],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.overrideProvider(CalendarsService)
.useClass(CalendarsServiceMock)
.compile();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization" });
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.createOAuthManagedUser("office365-connect@gmail.com", oAuthClient.id);
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClient.id);
accessTokenSecret = tokens.accessToken;
refreshTokenSecret = tokens.refreshToken;
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: [CLIENT_REDIRECT_URI],
permissions: 32,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it("should be defined", () => {
expect(oauthClientRepositoryFixture).toBeDefined();
expect(userRepositoryFixture).toBeDefined();
expect(oAuthClient).toBeDefined();
expect(accessTokenSecret).toBeDefined();
expect(refreshTokenSecret).toBeDefined();
expect(user).toBeDefined();
});
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/connect: it should respond 401 with invalid access token`, async () => {
await request(app.getHttpServer())
.get(`/v2/calendars/${OFFICE_365_CALENDAR}/connect`)
.set("Authorization", `Bearer invalid_access_token`)
.expect(401);
});
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/connect: it should redirect to auth-url for office 365 calendar oauth with valid access token `, async () => {
const response = await request(app.getHttpServer())
.get(`/v2/calendars/${OFFICE_365_CALENDAR}/connect`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.set("Origin", CLIENT_REDIRECT_URI)
.expect(200);
const data = response.body.data;
expect(data.authUrl).toBeDefined();
});
it(`/GET/v2/calendars/${GOOGLE_CALENDAR}/connect: it should redirect to auth-url for google calendar oauth with valid access token `, async () => {
const response = await request(app.getHttpServer())
.get(`/v2/calendars/${GOOGLE_CALENDAR}/connect`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.set("Origin", CLIENT_REDIRECT_URI)
.expect(200);
const data = response.body.data;
expect(data.authUrl).toBeDefined();
});
it(`/GET/v2/calendars/random-calendar/connect: it should respond 400 with a message saying the calendar type is invalid`, async () => {
await request(app.getHttpServer())
.get(`/v2/calendars/random-calendar/connect`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.set("Origin", CLIENT_REDIRECT_URI)
.expect(400);
});
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/save: without access token`, async () => {
await request(app.getHttpServer())
.get(
`/v2/calendars/${OFFICE_365_CALENDAR}/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=User.Read%20Calendars.Read%20Calendars.ReadWrite%20offline_access`
)
.expect(400);
});
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/save: without origin`, async () => {
await request(app.getHttpServer())
.get(
`/v2/calendars/${OFFICE_365_CALENDAR}/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=User.Read%20Calendars.Read%20Calendars.ReadWrite%20offline_access`
)
.expect(400);
});
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/check without access token`, async () => {
await request(app.getHttpServer()).get(`/v2/calendars/${OFFICE_365_CALENDAR}/check`).expect(401);
});
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/check with no credentials`, async () => {
await request(app.getHttpServer())
.get(`/v2/calendars/${OFFICE_365_CALENDAR}/check`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.set("Origin", CLIENT_REDIRECT_URI)
.expect(400);
});
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/check with access token, origin and office365 credentials`, async () => {
office365Credentials = await credentialsRepositoryFixture.create(
OFFICE_365_CALENDAR_TYPE,
{},
user.id,
OFFICE_365_CALENDAR_ID
);
await request(app.getHttpServer())
.get(`/v2/calendars/${OFFICE_365_CALENDAR}/check`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.set("Origin", CLIENT_REDIRECT_URI)
.expect(200);
});
it(`/GET/v2/calendars/${GOOGLE_CALENDAR}/check with access token, origin and google calendar credentials`, async () => {
googleCalendarCredentials = await credentialsRepositoryFixture.create(
GOOGLE_CALENDAR_TYPE,
{},
user.id,
GOOGLE_CALENDAR_ID
);
await request(app.getHttpServer())
.get(`/v2/calendars/${GOOGLE_CALENDAR}/check`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.set("Origin", CLIENT_REDIRECT_URI)
.expect(200);
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await credentialsRepositoryFixture.delete(office365Credentials.id);
await credentialsRepositoryFixture.delete(googleCalendarCredentials.id);
await userRepositoryFixture.deleteByEmail(user.email);
await app.close();
});
});
@@ -0,0 +1,12 @@
import { Request } from "express";
import { ApiResponse } from "@calcom/platform-types";
export interface CalendarApp {
save(state: string, code: string, origin: string): Promise<{ url: string }>;
check(userId: number): Promise<ApiResponse>;
}
export interface OAuthCalendarApp extends CalendarApp {
connect(authorization: string, req: Request): Promise<ApiResponse<{ authUrl: string }>>;
}
@@ -1,13 +1,25 @@
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
import { OutlookService } from "@/ee/calendars/services/outlook.service";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, UsersModule],
providers: [CredentialsRepository, CalendarsService],
imports: [PrismaModule, UsersModule, TokensModule],
providers: [
CredentialsRepository,
CalendarsService,
OutlookService,
GoogleCalendarService,
SelectedCalendarsRepository,
AppsRepository,
],
controllers: [CalendarsController],
exports: [CalendarsService],
})
@@ -1,24 +1,48 @@
import { GetBusyTimesOutput } from "@/ee/calendars/outputs/busy-times.output";
import { ConnectedCalendarsOutput } from "@/ee/calendars/outputs/connected-calendars.output";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
import { OutlookService } from "@/ee/calendars/services/outlook.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Controller, Get, UseGuards, Query } from "@nestjs/common";
import {
Controller,
Get,
UseGuards,
Query,
HttpStatus,
HttpCode,
Req,
Param,
Headers,
Redirect,
BadRequestException,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { Request } from "express";
import { z } from "zod";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { CalendarBusyTimesInput } from "@calcom/platform-types";
import { APPS_READ } from "@calcom/platform-constants";
import { SUCCESS_STATUS, CALENDARS, GOOGLE_CALENDAR, OFFICE_365_CALENDAR } from "@calcom/platform-constants";
import { ApiResponse, CalendarBusyTimesInput } from "@calcom/platform-types";
@Controller({
path: "/calendars",
version: "2",
path: "/v2/calendars",
version: API_VERSIONS_VALUES,
})
@UseGuards(AccessTokenGuard)
@DocsTags("Calendars")
export class CalendarsController {
constructor(private readonly calendarsService: CalendarsService) {}
constructor(
private readonly calendarsService: CalendarsService,
private readonly outlookService: OutlookService,
private readonly googleCalendarService: GoogleCalendarService
) {}
@UseGuards(ApiAuthGuard)
@Get("/busy-times")
async getBusyTimes(
@Query() queryParams: CalendarBusyTimesInput,
@@ -47,6 +71,7 @@ export class CalendarsController {
}
@Get("/")
@UseGuards(ApiAuthGuard)
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
const calendars = await this.calendarsService.getCalendars(userId);
@@ -55,4 +80,74 @@ export class CalendarsController {
data: calendars,
};
}
@UseGuards(ApiAuthGuard)
@Get("/:calendar/connect")
@HttpCode(HttpStatus.OK)
async redirect(
@Req() req: Request,
@Headers("Authorization") authorization: string,
@Param("calendar") calendar: string,
@Query("redir") redir?: string | null
): Promise<ApiResponse<{ authUrl: string }>> {
switch (calendar) {
case OFFICE_365_CALENDAR:
return await this.outlookService.connect(authorization, req, redir ?? "");
case GOOGLE_CALENDAR:
return await this.googleCalendarService.connect(authorization, req, redir ?? "");
default:
throw new BadRequestException(
"Invalid calendar type, available calendars are: ",
CALENDARS.join(", ")
);
}
}
@Get("/:calendar/save")
@HttpCode(HttpStatus.OK)
@Redirect(undefined, 301)
async save(
@Query("state") state: string,
@Query("code") code: string,
@Param("calendar") calendar: string
): Promise<{ url: string }> {
// state params contains our user access token
const stateParams = new URLSearchParams(state);
const { accessToken, origin, redir } = z
.object({ accessToken: z.string(), origin: z.string(), redir: z.string().nullish().optional() })
.parse({
accessToken: stateParams.get("accessToken"),
origin: stateParams.get("origin"),
redir: stateParams.get("redir"),
});
switch (calendar) {
case OFFICE_365_CALENDAR:
return await this.outlookService.save(code, accessToken, origin, redir ?? "");
case GOOGLE_CALENDAR:
return await this.googleCalendarService.save(code, accessToken, origin, redir ?? "");
default:
throw new BadRequestException(
"Invalid calendar type, available calendars are: ",
CALENDARS.join(", ")
);
}
}
@Get("/:calendar/check")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard, PermissionsGuard)
@Permissions([APPS_READ])
async check(@GetUser("id") userId: number, @Param("calendar") calendar: string): Promise<ApiResponse> {
switch (calendar) {
case OFFICE_365_CALENDAR:
return await this.outlookService.check(userId);
case GOOGLE_CALENDAR:
return await this.googleCalendarService.check(userId);
default:
throw new BadRequestException(
"Invalid calendar type, available calendars are: ",
CALENDARS.join(", ")
);
}
}
}
@@ -1,3 +1,4 @@
import { AppsRepository } from "@/modules/apps/apps.repository";
import {
CredentialsRepository,
CredentialsWithUserEmail,
@@ -11,21 +12,27 @@ import {
UnauthorizedException,
NotFoundException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { User } from "@prisma/client";
import { DateTime } from "luxon";
import { z } from "zod";
import { getConnectedDestinationCalendars } from "@calcom/platform-libraries";
import { getBusyCalendarTimes } from "@calcom/platform-libraries";
import { getConnectedDestinationCalendars } from "@calcom/platform-libraries-0.0.2";
import { getBusyCalendarTimes } from "@calcom/platform-libraries-0.0.2";
import { Calendar } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
@Injectable()
export class CalendarsService {
private oAuthCalendarResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
constructor(
private readonly usersRepository: UsersRepository,
private readonly credentialsRepository: CredentialsRepository,
private readonly appsRepository: AppsRepository,
private readonly dbRead: PrismaReadService,
private readonly dbWrite: PrismaWriteService
private readonly dbWrite: PrismaWriteService,
private readonly config: ConfigService
) {}
async getCalendars(userId: number) {
@@ -110,4 +117,24 @@ export class CalendarsService {
});
return composedSelectedCalendars;
}
async getAppKeys(appName: string) {
const app = await this.appsRepository.getAppBySlug(appName);
if (!app) {
throw new NotFoundException();
}
const { client_id, client_secret } = this.oAuthCalendarResponseSchema.parse(app.keys);
if (!client_id) {
throw new NotFoundException();
}
if (!client_secret) {
throw new NotFoundException();
}
return { client_id, client_secret };
}
}
@@ -0,0 +1,162 @@
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { Logger, NotFoundException } from "@nestjs/common";
import { BadRequestException, UnauthorizedException } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Prisma } from "@prisma/client";
import { Request } from "express";
import { google } from "googleapis";
import { z } from "zod";
import { SUCCESS_STATUS, GOOGLE_CALENDAR_TYPE } from "@calcom/platform-constants";
const CALENDAR_SCOPES = [
"https://www.googleapis.com/auth/calendar.readonly",
"https://www.googleapis.com/auth/calendar.events",
];
@Injectable()
export class GoogleCalendarService implements OAuthCalendarApp {
private redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
private gcalResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
private logger = new Logger("GcalService");
constructor(
private readonly config: ConfigService,
private readonly appsRepository: AppsRepository,
private readonly credentialRepository: CredentialsRepository,
private readonly calendarsService: CalendarsService,
private readonly tokensRepository: TokensRepository,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
) {}
async connect(
authorization: string,
req: Request,
redir?: string
): Promise<{ status: typeof SUCCESS_STATUS; data: { authUrl: string } }> {
const accessToken = authorization.replace("Bearer ", "");
const origin = req.get("origin") ?? req.get("host");
const redirectUrl = await await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
}
async save(code: string, accessToken: string, origin: string, redir?: string): Promise<{ url: string }> {
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir);
}
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
return await this.checkIfCalendarConnected(userId);
}
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string) {
const oAuth2Client = await this.getOAuthClient(this.redirectUri);
const authUrl = oAuth2Client.generateAuthUrl({
access_type: "offline",
scope: CALENDAR_SCOPES,
prompt: "consent",
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}`,
});
return authUrl;
}
async getOAuthClient(redirectUri: string) {
this.logger.log("Getting Google Calendar OAuth Client");
const app = await this.appsRepository.getAppBySlug("google-calendar");
if (!app) {
throw new NotFoundException();
}
const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
return oAuth2Client;
}
async checkIfCalendarConnected(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
const gcalCredentials = await this.credentialRepository.getByTypeAndUserId("google_calendar", userId);
if (!gcalCredentials) {
throw new BadRequestException("Credentials for google_calendar not found.");
}
if (gcalCredentials.invalid) {
throw new BadRequestException("Invalid google oauth credentials.");
}
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
const googleCalendar = connectedCalendars.find(
(cal: { integration: { type: string } }) => cal.integration.type === GOOGLE_CALENDAR_TYPE
);
if (!googleCalendar) {
throw new UnauthorizedException("Google Calendar not connected.");
}
if (googleCalendar.error?.message) {
throw new UnauthorizedException(googleCalendar.error?.message);
}
return { status: SUCCESS_STATUS };
}
async saveCalendarCredentialsAndRedirect(
code: string,
accessToken: string,
origin: string,
redir?: string
) {
// User chose not to authorize your app or didn't authorize your app
// redirect directly without oauth code
if (!code) {
return { url: redir || origin };
}
const parsedCode = z.string().parse(code);
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
if (!ownerId) {
throw new UnauthorizedException("Invalid Access token.");
}
const oAuth2Client = await this.getOAuthClient(this.redirectUri);
const token = await oAuth2Client.getToken(parsedCode);
// Google oAuth Credentials are stored in token.tokens
const key = token.tokens;
const credential = await this.credentialRepository.createAppCredential(
GOOGLE_CALENDAR_TYPE,
key as Prisma.InputJsonValue,
ownerId
);
oAuth2Client.setCredentials(key);
const calendar = google.calendar({
version: "v3",
auth: oAuth2Client,
});
const cals = await calendar.calendarList.list({ fields: "items(id,summary,primary,accessRole)" });
const primaryCal = cals.data.items?.find((cal) => cal.primary);
if (primaryCal?.id) {
await this.selectedCalendarsRepository.createSelectedCalendar(
primaryCal.id,
credential.id,
ownerId,
GOOGLE_CALENDAR_TYPE
);
}
return { url: redir || origin };
}
}
@@ -0,0 +1,189 @@
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import type { Calendar as OfficeCalendar } from "@microsoft/microsoft-graph-types-beta";
import { BadRequestException, UnauthorizedException } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Request } from "express";
import { stringify } from "querystring";
import { z } from "zod";
import {
SUCCESS_STATUS,
OFFICE_365_CALENDAR,
OFFICE_365_CALENDAR_ID,
OFFICE_365_CALENDAR_TYPE,
} from "@calcom/platform-constants";
@Injectable()
export class OutlookService implements OAuthCalendarApp {
private redirectUri = `${this.config.get("api.url")}/calendars/${OFFICE_365_CALENDAR}/save`;
constructor(
private readonly config: ConfigService,
private readonly calendarsService: CalendarsService,
private readonly credentialRepository: CredentialsRepository,
private readonly tokensRepository: TokensRepository,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
) {}
async connect(
authorization: string,
req: Request,
redir?: string
): Promise<{ status: typeof SUCCESS_STATUS; data: { authUrl: string } }> {
const accessToken = authorization.replace("Bearer ", "");
const origin = req.get("origin") ?? req.get("host");
const redirectUrl = await await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
}
async save(code: string, accessToken: string, origin: string, redir?: string): Promise<{ url: string }> {
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir);
}
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
return await this.checkIfCalendarConnected(userId);
}
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string) {
const { client_id } = await this.calendarsService.getAppKeys(OFFICE_365_CALENDAR_ID);
const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
const params = {
response_type: "code",
scope: scopes.join(" "),
client_id,
prompt: "select_account",
redirect_uri: this.redirectUri,
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}`,
};
const query = stringify(params);
const url = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${query}`;
return url;
}
async checkIfCalendarConnected(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
const office365CalendarCredentials = await this.credentialRepository.getByTypeAndUserId(
"office365_calendar",
userId
);
if (!office365CalendarCredentials) {
throw new BadRequestException("Credentials for office_365_calendar not found.");
}
if (office365CalendarCredentials.invalid) {
throw new BadRequestException("Invalid office 365 calendar credentials.");
}
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
const office365Calendar = connectedCalendars.find(
(cal: { integration: { type: string } }) => cal.integration.type === OFFICE_365_CALENDAR_TYPE
);
if (!office365Calendar) {
throw new UnauthorizedException("Office 365 calendar not connected.");
}
if (office365Calendar.error?.message) {
throw new UnauthorizedException(office365Calendar.error?.message);
}
return {
status: SUCCESS_STATUS,
};
}
async getOAuthCredentials(code: string) {
const scopes = ["offline_access", "Calendars.Read", "Calendars.ReadWrite"];
const { client_id, client_secret } = await this.calendarsService.getAppKeys(OFFICE_365_CALENDAR_ID);
const toUrlEncoded = (payload: Record<string, string>) =>
Object.keys(payload)
.map((key) => `${key}=${encodeURIComponent(payload[key])}`)
.join("&");
const body = toUrlEncoded({
client_id,
grant_type: "authorization_code",
code,
scope: scopes.join(" "),
redirect_uri: this.redirectUri,
client_secret,
});
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
body,
});
const responseBody = await response.json();
return responseBody;
}
async getDefaultCalendar(accessToken: string): Promise<OfficeCalendar> {
const response = await fetch("https://graph.microsoft.com/v1.0/me/calendar", {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
const responseBody = await response.json();
return responseBody as OfficeCalendar;
}
async saveCalendarCredentialsAndRedirect(
code: string,
accessToken: string,
origin: string,
redir?: string
) {
// if code is not defined, user denied to authorize office 365 app, just redirect straight away
if (!code) {
return { url: redir || origin };
}
const parsedCode = z.string().parse(code);
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
if (!ownerId) {
throw new UnauthorizedException("Invalid Access token.");
}
const office365OAuthCredentials = await this.getOAuthCredentials(parsedCode);
const defaultCalendar = await this.getDefaultCalendar(office365OAuthCredentials.access_token);
if (defaultCalendar?.id) {
const credential = await this.credentialRepository.createAppCredential(
OFFICE_365_CALENDAR_TYPE,
office365OAuthCredentials,
ownerId
);
await this.selectedCalendarsRepository.createSelectedCalendar(
defaultCalendar.id,
credential.id,
ownerId,
OFFICE_365_CALENDAR_TYPE
);
}
return {
url: redir || origin,
};
}
}
@@ -1,17 +0,0 @@
import { EventTypesController } from "@/ee/event-types/controllers/event-types.controller";
import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule],
providers: [EventTypesRepository, EventTypesService],
controllers: [EventTypesController],
exports: [EventTypesService, EventTypesRepository],
})
export class EventTypesModule {}
@@ -1,13 +1,13 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { EventTypesModule } from "@/ee/event-types/event-types.module";
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
import { Editable } from "@/ee/event-types/inputs/enums/editable";
import { BaseField } from "@/ee/event-types/inputs/enums/field-type";
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
import { Editable } from "@/ee/event-types/event-types_2024_04_15//inputs/enums/editable";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
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 { GetEventTypePublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output";
import { GetEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output";
import { GetEventTypesPublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
@@ -22,15 +22,20 @@ import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-type
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import {
SUCCESS_STATUS,
VERSION_2024_06_11,
VERSION_2024_04_15,
CAL_API_VERSION_HEADER,
} from "@calcom/platform-constants";
import {
EventTypesByViewer,
EventTypesPublic,
eventTypeBookingFields,
eventTypeLocations,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries-0.0.2";
import { ApiSuccessResponse } from "@calcom/platform-types";
describe("Event types Endpoints", () => {
@@ -40,7 +45,7 @@ describe("Event types Endpoints", () => {
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, EventTypesModule, TokensModule],
imports: [AppModule, UsersModule, EventTypesModule_2024_04_15, TokensModule],
})
.overrideGuard(PermissionsGuard)
.useValue({
@@ -79,11 +84,11 @@ describe("Event types Endpoints", () => {
let user: User;
beforeAll(async () => {
const moduleRef = await withAccessTokenAuth(
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, EventTypesModule, TokensModule],
imports: [AppModule, UsersModule, EventTypesModule_2024_04_15, TokensModule],
})
)
.overrideGuard(PermissionsGuard)
@@ -132,7 +137,7 @@ describe("Event types Endpoints", () => {
});
it("should create an event type", async () => {
const body: CreateEventTypeInput = {
const body: CreateEventTypeInput_2024_04_15 = {
title: "Test Event Type",
slug: "test-event-type",
description: "A description of the test event type.",
@@ -154,6 +159,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
.send(body)
.expect(201)
.then(async (response) => {
@@ -173,7 +179,7 @@ describe("Event types Endpoints", () => {
it("should update event type", async () => {
const newTitle = "Updated title";
const body: UpdateEventTypeInput = {
const body: UpdateEventTypeInput_2024_04_15 = {
title: newTitle,
disableGuests: false,
slotInterval: 30,
@@ -184,6 +190,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
.send(body)
.expect(200)
.then(async (response) => {
@@ -204,10 +211,20 @@ describe("Event types Endpoints", () => {
});
});
it("should return 400 if param event type id is null", async () => {
const locations = [{ type: "inPerson", address: "123 Main St" }];
const body: UpdateEventTypeInput_2024_04_15 = {
locations,
};
return request(app.getHttpServer()).patch(`/api/v2/event-types/null`).send(body).expect(400);
});
it("should update event type locations", async () => {
const locations = [{ type: "inPerson", address: "123 Main St" }];
const body: UpdateEventTypeInput = {
const body: UpdateEventTypeInput_2024_04_15 = {
locations,
};
@@ -256,7 +273,7 @@ describe("Event types Endpoints", () => {
},
];
const body: UpdateEventTypeInput = {
const body: UpdateEventTypeInput_2024_04_15 = {
bookingFields,
};
@@ -278,7 +295,26 @@ describe("Event types Endpoints", () => {
it(`/GET/:id`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${eventType.id}`)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: GetEventTypeOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.eventType.id).toEqual(eventType.id);
expect(responseBody.data.eventType.title).toEqual(eventType.title);
expect(responseBody.data.eventType.slug).toEqual(eventType.slug);
expect(responseBody.data.eventType.userId).toEqual(user.id);
});
it(`/GET/:id with version VERSION_2024_06_11`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_11)
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
@@ -295,7 +331,8 @@ describe("Event types Endpoints", () => {
it(`/GET/:username/public`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${username}/public`)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
@@ -313,7 +350,8 @@ describe("Event types Endpoints", () => {
it(`/GET/:username/:eventSlug/public`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${username}/${eventType.slug}/public`)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
@@ -330,7 +368,8 @@ describe("Event types Endpoints", () => {
it(`/GET/`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types`)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
@@ -349,7 +388,8 @@ describe("Event types Endpoints", () => {
it(`/GET/public/:username/`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${username}/public`)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
@@ -365,13 +405,17 @@ describe("Event types Endpoints", () => {
it(`/GET/:id not existing`, async () => {
await request(app.getHttpServer())
.get(`/api/v2/event-types/1000`)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(404);
});
it("should delete schedule", async () => {
return request(app.getHttpServer()).delete(`/api/v2/event-types/${eventType.id}`).expect(200);
return request(app.getHttpServer())
.delete(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
.expect(200);
});
afterAll(async () => {
@@ -1,17 +1,22 @@
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
import { GetPublicEventTypeQueryParams } from "@/ee/event-types/inputs/get-public-event-type-query-params.input";
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
import { DeleteEventTypeOutput } from "@/ee/event-types/outputs/delete-event-type.output";
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
import { GetEventTypesData, GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
import { UpdateEventTypeOutput } from "@/ee/event-types/outputs/update-event-type.output";
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { EventTypeIdParams_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-id.input";
import { GetPublicEventTypeQueryParams_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/get-public-event-type-query-params.input";
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
import { CreateEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/create-event-type.output";
import { DeleteEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/delete-event-type.output";
import { GetEventTypePublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output";
import { GetEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output";
import { GetEventTypesPublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output";
import {
GetEventTypesData,
GetEventTypesOutput,
} from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-types.output";
import { UpdateEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/update-event-type.output";
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
import { VERSION_2024_04_15, VERSION_2024_06_11 } from "@/lib/api-versions";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { UserWithProfile } from "@/modules/users/users.repository";
@@ -29,31 +34,32 @@ import {
Delete,
Query,
InternalServerErrorException,
ParseIntPipe,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { getPublicEvent } from "@calcom/platform-libraries";
import { getEventTypesByViewer } from "@calcom/platform-libraries";
import { getPublicEvent } from "@calcom/platform-libraries-0.0.2";
import { getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
import { PrismaClient } from "@calcom/prisma";
@Controller({
path: "event-types",
version: "2",
path: "/v2/event-types",
version: [VERSION_2024_04_15, VERSION_2024_06_11],
})
@UseGuards(PermissionsGuard)
@DocsTags("Event types")
export class EventTypesController {
export class EventTypesController_2024_04_15 {
constructor(
private readonly eventTypesService: EventTypesService,
private readonly eventTypesService: EventTypesService_2024_04_15,
private readonly prismaReadService: PrismaReadService
) {}
@Post("/")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
async createEventType(
@Body() body: CreateEventTypeInput,
@Body() body: CreateEventTypeInput_2024_04_15,
@GetUser() user: UserWithProfile
): Promise<CreateEventTypeOutput> {
const eventType = await this.eventTypesService.createUserEventType(user, body);
@@ -66,9 +72,9 @@ export class EventTypesController {
@Get("/:eventTypeId")
@Permissions([EVENT_TYPE_READ])
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
async getEventType(
@Param("eventTypeId") eventTypeId: string,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@GetUser() user: UserWithProfile
): Promise<GetEventTypeOutput> {
const eventType = await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId));
@@ -85,7 +91,7 @@ export class EventTypesController {
@Get("/")
@Permissions([EVENT_TYPE_READ])
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
async getEventTypes(@GetUser() user: UserWithProfile): Promise<GetEventTypesOutput> {
const eventTypes = await getEventTypesByViewer({
id: user.id,
@@ -104,7 +110,7 @@ export class EventTypesController {
async getPublicEventType(
@Param("username") username: string,
@Param("eventSlug") eventSlug: string,
@Query() queryParams: GetPublicEventTypeQueryParams
@Query() queryParams: GetPublicEventTypeQueryParams_2024_04_15
): Promise<GetEventTypePublicOutput> {
try {
const event = await getPublicEvent(
@@ -141,11 +147,12 @@ export class EventTypesController {
@Patch("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
@HttpCode(HttpStatus.OK)
async updateEventType(
@Param("eventTypeId") eventTypeId: number,
@Body() body: UpdateEventTypeInput,
@Param() params: EventTypeIdParams_2024_04_15,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@Body() body: UpdateEventTypeInput_2024_04_15,
@GetUser() user: UserWithProfile
): Promise<UpdateEventTypeOutput> {
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
@@ -158,9 +165,10 @@ export class EventTypesController {
@Delete("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
async deleteEventType(
@Param("eventTypeId") eventTypeId: number,
@Param() params: EventTypeIdParams_2024_04_15,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@GetUser("id") userId: number
): Promise<DeleteEventTypeOutput> {
const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId);
@@ -0,0 +1,17 @@
import { EventTypesController_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/controllers/event-types.controller";
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule],
providers: [EventTypesRepository_2024_04_15, EventTypesService_2024_04_15],
controllers: [EventTypesController_2024_04_15],
exports: [EventTypesService_2024_04_15, EventTypesRepository_2024_04_15],
})
export class EventTypesModule_2024_04_15 {}
@@ -1,19 +1,19 @@
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { getEventTypeById } from "@calcom/platform-libraries";
import { getEventTypeById } from "@calcom/platform-libraries-0.0.2";
import type { PrismaClient } from "@calcom/prisma";
@Injectable()
export class EventTypesRepository {
export class EventTypesRepository_2024_04_15 {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async createUserEventType(
userId: number,
body: Pick<CreateEventTypeInput, "title" | "slug" | "length" | "hidden">
body: Pick<CreateEventTypeInput_2024_04_15, "title" | "slug" | "length" | "hidden">
) {
return this.dbWrite.prisma.eventType.create({
data: {
@@ -1,4 +1,4 @@
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
@@ -20,7 +20,7 @@ export const CREATE_EVENT_DESCRIPTION_EXAMPLE =
// note(Lauris): We will gradually expose more properties if any customer needs them.
// Just uncomment any below when requested.
export class CreateEventTypeInput {
export class CreateEventTypeInput_2024_04_15 {
@IsNumber()
@Min(1)
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
@@ -46,9 +46,9 @@ export class CreateEventTypeInput {
@IsOptional()
@ValidateNested({ each: true })
@Type(() => EventTypeLocation)
@Type(() => EventTypeLocation_2024_04_15)
@IsArray()
locations?: EventTypeLocation[];
locations?: EventTypeLocation_2024_04_15[];
@IsBoolean()
@IsOptional()
@@ -0,0 +1,6 @@
import { IsNumberString } from "class-validator";
export class EventTypeIdParams_2024_04_15 {
@IsNumberString()
eventTypeId!: number;
}
@@ -4,7 +4,7 @@ import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validato
// note(Lauris): We will gradually expose more properties if any customer needs them.
// Just uncomment any below when requested.
export class EventTypeLocation {
export class EventTypeLocation_2024_04_15 {
@IsString()
@DocsProperty({ example: "link" })
type!: string;
@@ -2,7 +2,7 @@ import { ApiProperty } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsBoolean, IsOptional, IsString } from "class-validator";
export class GetPublicEventTypeQueryParams {
export class GetPublicEventTypeQueryParams_2024_04_15 {
@Transform(({ value }: { value: string }) => value === "true")
@IsBoolean()
@IsOptional()
@@ -1,7 +1,7 @@
import { Editable } from "@/ee/event-types/inputs/enums/editable";
import { BaseField } from "@/ee/event-types/inputs/enums/field-type";
import { Frequency } from "@/ee/event-types/inputs/enums/frequency";
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.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 { Frequency } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/frequency";
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
import { Type } from "class-transformer";
import {
IsString,
@@ -105,7 +105,7 @@ class VariantsConfig {
variants!: Record<string, Variant>;
}
export class BookingField {
export class BookingField_2024_04_15 {
@IsEnum(BaseField)
type!: BaseField;
@@ -179,7 +179,7 @@ export class BookingField {
sources?: Source[];
}
export class RecurringEvent {
export class RecurringEvent_2024_04_15 {
@IsDate()
@IsOptional()
dtstart?: Date;
@@ -202,7 +202,7 @@ export class RecurringEvent {
tzid?: string;
}
export class IntervalLimits {
export class IntervalLimits_2024_04_15 {
@IsNumber()
@IsOptional()
PER_DAY?: number;
@@ -220,7 +220,7 @@ export class IntervalLimits {
PER_YEAR?: number;
}
export class UpdateEventTypeInput {
export class UpdateEventTypeInput_2024_04_15 {
@IsInt()
@Min(1)
@IsOptional()
@@ -243,9 +243,9 @@ export class UpdateEventTypeInput {
hidden?: boolean;
@ValidateNested({ each: true })
@Type(() => EventTypeLocation)
@Type(() => EventTypeLocation_2024_04_15)
@IsOptional()
locations?: EventTypeLocation[];
locations?: EventTypeLocation_2024_04_15[];
// @IsInt()
// @IsOptional()
@@ -278,8 +278,8 @@ export class UpdateEventTypeInput {
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => BookingField)
bookingFields?: BookingField[];
@Type(() => BookingField_2024_04_15)
bookingFields?: BookingField_2024_04_15[];
// @IsString()
// @IsOptional()
@@ -1,4 +1,4 @@
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
@@ -2,7 +2,7 @@ import {
CREATE_EVENT_LENGTH_EXAMPLE,
CREATE_EVENT_SLUG_EXAMPLE,
CREATE_EVENT_TITLE_EXAMPLE,
} from "@/ee/event-types/inputs/create-event-type.input";
} from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
@@ -3,15 +3,15 @@ import {
CREATE_EVENT_LENGTH_EXAMPLE,
CREATE_EVENT_SLUG_EXAMPLE,
CREATE_EVENT_TITLE_EXAMPLE,
} from "@/ee/event-types/inputs/create-event-type.input";
import { PeriodType } from "@/ee/event-types/inputs/enums/period-type";
import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type";
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
} from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { PeriodType } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/period-type";
import { SchedulingType } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/scheduling-type";
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
import {
BookingField,
IntervalLimits,
RecurringEvent,
} from "@/ee/event-types/inputs/update-event-type.input";
BookingField_2024_04_15,
IntervalLimits_2024_04_15,
RecurringEvent_2024_04_15,
} from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
@@ -53,9 +53,9 @@ export class EventTypeOutput {
hidden!: boolean;
@ValidateNested({ each: true })
@Type(() => EventTypeLocation)
@Type(() => EventTypeLocation_2024_04_15)
@IsArray()
locations!: EventTypeLocation[] | null;
locations!: EventTypeLocation_2024_04_15[] | null;
@IsInt()
@ApiHideProperty()
@@ -91,9 +91,9 @@ export class EventTypeOutput {
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => BookingField)
@Type(() => BookingField_2024_04_15)
@ApiHideProperty()
bookingFields!: BookingField[] | null;
bookingFields!: BookingField_2024_04_15[] | null;
@IsString()
@ApiHideProperty()
@@ -132,10 +132,10 @@ export class EventTypeOutput {
requiresBookerEmailVerification!: boolean;
@ValidateNested()
@Type(() => RecurringEvent)
@Type(() => RecurringEvent_2024_04_15)
@IsOptional()
@ApiHideProperty()
recurringEvent!: RecurringEvent | null;
recurringEvent!: RecurringEvent_2024_04_15 | null;
@IsBoolean()
@ApiHideProperty()
@@ -203,15 +203,15 @@ export class EventTypeOutput {
successRedirectUrl!: string | null;
@ValidateNested()
@Type(() => IntervalLimits)
@Type(() => IntervalLimits_2024_04_15)
@IsOptional()
@ApiHideProperty()
bookingLimits!: IntervalLimits;
bookingLimits!: IntervalLimits_2024_04_15;
@ValidateNested()
@Type(() => IntervalLimits)
@Type(() => IntervalLimits_2024_04_15)
@ApiHideProperty()
durationLimits!: IntervalLimits;
durationLimits!: IntervalLimits_2024_04_15;
@IsBoolean()
@ApiHideProperty()
@@ -1,4 +1,4 @@
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, ValidateNested } from "class-validator";
@@ -2,7 +2,7 @@ import {
CREATE_EVENT_LENGTH_EXAMPLE,
CREATE_EVENT_SLUG_EXAMPLE,
CREATE_EVENT_TITLE_EXAMPLE,
} from "@/ee/event-types/inputs/create-event-type.input";
} from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
@@ -1,4 +1,4 @@
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsEnum, ValidateNested } from "class-validator";
@@ -1,4 +1,4 @@
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
@@ -1,29 +1,32 @@
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/constants/constants";
import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
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 { 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";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
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 { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.2";
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries-0.0.2";
import { EventType } from "@calcom/prisma/client";
@Injectable()
export class EventTypesService {
export class EventTypesService_2024_04_15 {
constructor(
private readonly eventTypesRepository: EventTypesRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_04_15,
private readonly membershipsRepository: MembershipsRepository,
private readonly usersRepository: UsersRepository,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
private readonly dbWrite: PrismaWriteService
) {}
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput): Promise<EventTypeOutput> {
async createUserEventType(
user: UserWithProfile,
body: CreateEventTypeInput_2024_04_15
): Promise<EventTypeOutput> {
await this.checkCanCreateEventType(user.id, body);
const eventTypeUser = await this.getUserToCreateEvent(user);
const { eventType } = await createEventType({
@@ -38,7 +41,7 @@ export class EventTypesService {
return eventType as EventTypeOutput;
}
async checkCanCreateEventType(userId: number, body: CreateEventTypeInput) {
async checkCanCreateEventType(userId: number, body: CreateEventTypeInput_2024_04_15) {
const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug);
if (existsWithSlug) {
throw new BadRequestException("User already has an event type with this slug.");
@@ -114,7 +117,7 @@ export class EventTypesService {
return defaultEventTypes;
}
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput, user: UserWithProfile) {
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_04_15, user: UserWithProfile) {
this.checkCanUpdateEventType(user.id, eventTypeId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
await updateEventType({
@@ -0,0 +1,16 @@
export const DEFAULT_EVENT_TYPES = {
thirtyMinutes: { length: 30, slug: "thirty-minutes", title: "30 Minutes" },
thirtyMinutesVideo: {
length: 30,
slug: "thirty-minutes-video",
title: "30 Minutes",
locations: [{ type: "integrations:daily" }],
},
sixtyMinutes: { length: 60, slug: "sixty-minutes", title: "60 Minutes" },
sixtyMinutesVideo: {
length: 60,
slug: "sixty-minutes-video",
title: "60 Minutes",
locations: [{ type: "integrations:daily" }],
},
};
@@ -0,0 +1,295 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User } from "@prisma/client";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
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,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
describe("Event types Endpoints", () => {
describe("Not authenticated", () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it(`/GET/:id`, () => {
return request(app.getHttpServer()).get("/api/v2/event-types/100").expect(401);
});
afterAll(async () => {
await app.close();
});
});
describe("User Authenticated", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
const userEmail = "event-types-test-e2e@api.com";
const name = "bob-the-builder";
const username = name;
let eventType: EventTypeOutput_2024_06_14;
let user: User;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
})
)
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization" });
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
name,
username,
});
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["redirect-uri"],
permissions: 32,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it("should be defined", () => {
expect(oauthClientRepositoryFixture).toBeDefined();
expect(userRepositoryFixture).toBeDefined();
expect(oAuthClient).toBeDefined();
expect(user).toBeDefined();
});
it("should create an event type", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class",
slug: "coding-class",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
bookingFields: [
{
type: "select",
label: "select which language you want to learn",
slug: "select-language",
required: true,
placeholder: "select language",
options: ["javascript", "python", "cobol"],
},
],
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.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.locations).toEqual(body.locations);
expect(createdEventType.bookingFields).toEqual(body.bookingFields);
expect(createdEventType.ownerId).toEqual(user.id);
eventType = responseBody.data;
});
});
it("should update event type", async () => {
const newTitle = "Coding class in Italian!";
const body: UpdateEventTypeInput_2024_06_14 = {
title: newTitle,
};
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const updatedEventType = responseBody.data;
expect(updatedEventType.title).toEqual(body.title);
expect(updatedEventType.id).toEqual(eventType.id);
expect(updatedEventType.title).toEqual(newTitle);
expect(updatedEventType.description).toEqual(eventType.description);
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(updatedEventType.locations).toEqual(eventType.locations);
expect(updatedEventType.bookingFields).toEqual(eventType.bookingFields);
expect(updatedEventType.ownerId).toEqual(user.id);
eventType.title = newTitle;
});
});
it(`/GET/:id`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(fetchedEventType.id).toEqual(eventType.id);
expect(fetchedEventType.title).toEqual(eventType.title);
expect(fetchedEventType.description).toEqual(eventType.description);
expect(fetchedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(fetchedEventType.locations).toEqual(eventType.locations);
expect(fetchedEventType.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType.ownerId).toEqual(user.id);
});
it(`/GET/even-types by username`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${username}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(1);
const fetchedEventType = responseBody.data?.[0];
expect(fetchedEventType?.id).toEqual(eventType.id);
expect(fetchedEventType?.title).toEqual(eventType.title);
expect(fetchedEventType?.description).toEqual(eventType.description);
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType?.ownerId).toEqual(user.id);
});
it(`/GET/event-types by username and eventSlug`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${username}&eventSlug=${eventType.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
const fetchedEventType = responseBody.data[0];
expect(fetchedEventType?.id).toEqual(eventType.id);
expect(fetchedEventType?.title).toEqual(eventType.title);
expect(fetchedEventType?.description).toEqual(eventType.description);
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType?.ownerId).toEqual(user.id);
});
it(`/GET/:id not existing`, async () => {
await request(app.getHttpServer())
.get(`/api/v2/event-types/1000`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(404);
});
it("should delete schedule", async () => {
return request(app.getHttpServer()).delete(`/api/v2/event-types/${eventType.id}`).expect(200);
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
try {
await eventTypesRepositoryFixture.delete(eventType.id);
} catch (e) {
// Event type might have been deleted by the test
}
try {
await userRepositoryFixture.delete(user.id);
} catch (e) {
// User might have been deleted by the test
}
await app.close();
});
});
});
@@ -0,0 +1,128 @@
import { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output";
import { DeleteEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/delete-event-type.output";
import { GetEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output";
import { GetEventTypesOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-types.output";
import { UpdateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/update-event-type.output";
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
UseGuards,
Get,
Param,
Post,
Body,
NotFoundException,
Patch,
HttpCode,
HttpStatus,
Delete,
Query,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import {
CreateEventTypeInput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
GetEventTypesQuery_2024_06_14,
} from "@calcom/platform-types";
@Controller({
path: "/v2/event-types",
version: VERSION_2024_06_14_VALUE,
})
@UseGuards(PermissionsGuard)
@DocsTags("Event types")
export class EventTypesController_2024_06_14 {
constructor(private readonly eventTypesService: EventTypesService_2024_06_14) {}
@Post("/")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
async createEventType(
@Body() body: CreateEventTypeInput_2024_06_14,
@GetUser() user: UserWithProfile
): Promise<CreateEventTypeOutput_2024_06_14> {
const eventType = await this.eventTypesService.createUserEventType(user, body);
return {
status: SUCCESS_STATUS,
data: eventType,
};
}
@Get("/:eventTypeId")
@Permissions([EVENT_TYPE_READ])
@UseGuards(ApiAuthGuard)
async getEventTypeById(
@Param("eventTypeId") eventTypeId: string,
@GetUser() user: UserWithProfile
): Promise<GetEventTypeOutput_2024_06_14> {
const eventType = await this.eventTypesService.getUserEventType(user.id, Number(eventTypeId));
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
return {
status: SUCCESS_STATUS,
data: eventType,
};
}
@Get("/")
@Permissions([EVENT_TYPE_READ])
async getEventTypes(
@Query() queryParams: GetEventTypesQuery_2024_06_14
): Promise<GetEventTypesOutput_2024_06_14> {
const eventTypes = await this.eventTypesService.getEventTypes(queryParams);
return {
status: SUCCESS_STATUS,
data: eventTypes,
};
}
@Patch("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
@HttpCode(HttpStatus.OK)
async updateEventType(
@Param("eventTypeId") eventTypeId: number,
@Body() body: UpdateEventTypeInput_2024_06_14,
@GetUser() user: UserWithProfile
): Promise<UpdateEventTypeOutput_2024_06_14> {
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
return {
status: SUCCESS_STATUS,
data: eventType,
};
}
@Delete("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
async deleteEventType(
@Param("eventTypeId") eventTypeId: number,
@GetUser("id") userId: number
): Promise<DeleteEventTypeOutput_2024_06_14> {
const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId);
return {
status: SUCCESS_STATUS,
data: {
id: eventType.id,
lengthInMinutes: eventType.length,
slug: eventType.slug,
title: eventType.title,
},
};
}
}
@@ -0,0 +1,27 @@
import { EventTypesController_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/controllers/event-types.controller";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule],
providers: [
EventTypesRepository_2024_06_14,
EventTypesService_2024_06_14,
InputEventTypesService_2024_06_14,
OutputEventTypesService_2024_06_14,
UsersRepository,
UsersService,
],
controllers: [EventTypesController_2024_06_14],
exports: [EventTypesService_2024_06_14, EventTypesRepository_2024_06_14],
})
export class EventTypesModule_2024_06_14 {}
@@ -0,0 +1,103 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import {
getEventTypeById,
transformApiEventTypeBookingFields,
transformApiEventTypeLocations,
} from "@calcom/platform-libraries-0.0.13";
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import type { PrismaClient } from "@calcom/prisma";
type InputEventTransformed = Omit<
CreateEventTypeInput_2024_06_14,
"lengthInMinutes" | "locations" | "bookingFields"
> & {
length: number;
slug: string;
locations?: ReturnType<typeof transformApiEventTypeLocations>;
bookingFields?: ReturnType<typeof transformApiEventTypeBookingFields>;
};
@Injectable()
export class EventTypesRepository_2024_06_14 {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async createUserEventType(userId: number, body: InputEventTransformed) {
return this.dbWrite.prisma.eventType.create({
data: {
...body,
userId,
locations: body.locations,
bookingFields: body.bookingFields,
users: { connect: { id: userId } },
},
});
}
async getEventTypeWithSeats(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
select: { users: { select: { id: true } }, seatsPerTimeSlot: true },
});
}
async getUserEventType(userId: number, eventTypeId: number) {
return this.dbRead.prisma.eventType.findFirst({
where: {
id: eventTypeId,
userId,
},
include: { users: true, schedule: true },
});
}
async getUserEventTypes(userId: number) {
return this.dbRead.prisma.eventType.findMany({
where: {
userId,
},
include: { users: true, schedule: true },
});
}
async getUserEventTypeForAtom(
user: UserWithProfile,
isUserOrganizationAdmin: boolean,
eventTypeId: number
) {
return await getEventTypeById({
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
eventTypeId,
userId: user.id,
prisma: this.dbRead.prisma as unknown as PrismaClient,
isUserOrganizationAdmin,
isTrpcCall: true,
});
}
async getEventTypeById(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: { users: true, schedule: true },
});
}
async getUserEventTypeBySlug(userId: number, slug: string) {
return this.dbRead.prisma.eventType.findUnique({
where: {
userId_slug: {
userId: userId,
slug: slug,
},
},
include: { users: true, schedule: true },
});
}
async deleteEventType(eventTypeId: number) {
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
}
}
@@ -0,0 +1,20 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsIn, IsNotEmptyObject, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types";
export class CreateEventTypeOutput_2024_06_14 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsIn([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: EventTypeOutput_2024_06_14,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => EventTypeOutput_2024_06_14)
data!: EventTypeOutput_2024_06_14;
}
@@ -0,0 +1,33 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsIn, IsInt, IsString } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { CREATE_EVENT_LENGTH_EXAMPLE, CREATE_EVENT_TITLE_EXAMPLE } from "@calcom/platform-types";
class DeleteData_2024_06_14 {
@IsInt()
@DocsProperty({ example: 1 })
id!: number;
@IsInt()
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
lengthInMinutes!: number;
@IsString()
slug!: string;
@IsString()
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
title!: string;
}
export class DeleteEventTypeOutput_2024_06_14 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsIn([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@Type(() => DeleteData_2024_06_14)
data!: DeleteData_2024_06_14;
}
@@ -0,0 +1,19 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsIn, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types";
export class GetEventTypeOutput_2024_06_14 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsIn([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: EventTypeOutput_2024_06_14,
})
@ValidateNested()
@Type(() => EventTypeOutput_2024_06_14)
data!: EventTypeOutput_2024_06_14 | null;
}
@@ -0,0 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsIn, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types";
export class GetEventTypesOutput_2024_06_14 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsIn([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ValidateNested({ each: true })
@Type(() => EventTypeOutput_2024_06_14)
@IsArray()
data!: EventTypeOutput_2024_06_14[];
}
@@ -0,0 +1,20 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsIn, IsNotEmptyObject, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types";
export class UpdateEventTypeOutput_2024_06_14 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsIn([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: EventTypeOutput_2024_06_14,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => EventTypeOutput_2024_06_14)
data!: EventTypeOutput_2024_06_14;
}
@@ -0,0 +1,257 @@
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_06_14/constants/constants";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
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-0.0.13";
import {
CreateEventTypeInput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
GetEventTypesQuery_2024_06_14,
EventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
import { EventType } from "@calcom/prisma/client";
@Injectable()
export class EventTypesService_2024_06_14 {
constructor(
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
private readonly membershipsRepository: MembershipsRepository,
private readonly usersRepository: UsersRepository,
private readonly usersService: UsersService,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
private readonly dbWrite: PrismaWriteService
) {}
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput_2024_06_14) {
await this.checkCanCreateEventType(user.id, body);
const eventTypeUser = await this.getUserToCreateEvent(user);
const bodyTransformed = this.inputEventTypesService.transformInputCreateEventType(body);
const { eventType: eventTypeCreated } = await createEventType({
input: bodyTransformed,
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prisma: this.dbWrite.prisma,
},
});
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeCreated.id);
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeCreated.id} not found`);
}
return this.outputEventTypesService.getResponseEventType(user.id, eventType);
}
async checkCanCreateEventType(userId: number, body: CreateEventTypeInput_2024_06_14) {
const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug);
if (existsWithSlug) {
throw new BadRequestException("User already has an event type with this slug.");
}
}
async getEventTypeByUsernameAndSlug(username: string, eventTypeSlug: string) {
const user = await this.usersRepository.findByUsername(username);
if (!user) {
return null;
}
const eventType = await this.eventTypesRepository.getUserEventTypeBySlug(user.id, eventTypeSlug);
if (!eventType) {
return null;
}
return this.outputEventTypesService.getResponseEventType(user.id, eventType);
}
async getEventTypesByUsername(username: string) {
const user = await this.usersRepository.findByUsername(username);
if (!user) {
return [];
}
return this.getUserEventTypes(user.id);
}
async getUserToCreateEvent(user: UserWithProfile) {
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
const isOrgAdmin = organizationId
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
: false;
const profileId = user.movedToProfile?.id || null;
return {
id: user.id,
organizationId: user.organizationId,
organization: { isOrgAdmin },
profile: { id: profileId },
metadata: user.metadata,
};
}
async getUserEventType(userId: number, eventTypeId: number) {
const eventType = await this.eventTypesRepository.getUserEventType(userId, eventTypeId);
if (!eventType) {
return null;
}
this.checkUserOwnsEventType(userId, eventType);
return this.outputEventTypesService.getResponseEventType(userId, eventType);
}
async getUserEventTypes(userId: number) {
const eventTypes = await this.eventTypesRepository.getUserEventTypes(userId);
const eventTypePromises = eventTypes.map(async (eventType) => {
return await this.outputEventTypesService.getResponseEventType(userId, eventType);
});
return await Promise.all(eventTypePromises);
}
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
const isUserOrganizationAdmin = organizationId
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
: false;
const eventType = await this.eventTypesRepository.getUserEventTypeForAtom(
user,
isUserOrganizationAdmin,
eventTypeId
);
if (!eventType) {
return null;
}
this.checkUserOwnsEventType(user.id, eventType.eventType);
return eventType;
}
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
const user = await this.usersRepository.findByUsername(username);
if (!user) {
throw new NotFoundException(`User with username "${username}" not found`);
}
return await getEventTypesPublic(user.id);
}
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14): Promise<EventTypeOutput_2024_06_14[]> {
const { username, eventSlug, usernames } = queryParams;
if (username && eventSlug) {
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug);
return eventType ? [eventType] : [];
}
if (username) {
return await this.getEventTypesByUsername(username);
}
if (usernames) {
const dynamicEventType = await this.getDynamicEventType(usernames);
return [dynamicEventType];
}
return [];
}
async getDynamicEventType(usernames: string[]) {
const users = await this.usersService.getByUsernames(usernames);
const usersFiltered: UserWithProfile[] = [];
for (const user of users) {
if (user) {
usersFiltered.push(user);
}
}
return this.outputEventTypesService.getResponseEventType(0, {
...dynamicEvent,
users: usersFiltered,
isInstantEvent: false,
});
}
async createUserDefaultEventTypes(userId: number) {
const { sixtyMinutes, sixtyMinutesVideo, thirtyMinutes, thirtyMinutesVideo } = DEFAULT_EVENT_TYPES;
const defaultEventTypes = await Promise.all([
this.eventTypesRepository.createUserEventType(userId, thirtyMinutes),
this.eventTypesRepository.createUserEventType(userId, sixtyMinutes),
this.eventTypesRepository.createUserEventType(userId, thirtyMinutesVideo),
this.eventTypesRepository.createUserEventType(userId, sixtyMinutesVideo),
]);
return defaultEventTypes;
}
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_06_14, user: UserWithProfile) {
this.checkCanUpdateEventType(user.id, eventTypeId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
const bodyTransformed = this.inputEventTypesService.transformInputUpdateEventType(body);
await updateEventType({
input: { id: eventTypeId, ...bodyTransformed },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prisma: this.dbWrite.prisma,
},
});
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
return this.outputEventTypesService.getResponseEventType(user.id, eventType);
}
async checkCanUpdateEventType(userId: number, eventTypeId: number) {
const existingEventType = await this.getUserEventType(userId, eventTypeId);
if (!existingEventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
this.checkUserOwnsEventType(userId, { id: eventTypeId, userId: existingEventType.ownerId });
}
async getUserToUpdateEvent(user: UserWithProfile) {
const profileId = user.movedToProfile?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
return { ...user, profile: { id: profileId }, selectedCalendars };
}
async deleteEventType(eventTypeId: number, userId: number) {
const existingEventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
if (!existingEventType) {
throw new NotFoundException(`Event type with ID=${eventTypeId} does not exist.`);
}
this.checkUserOwnsEventType(userId, existingEventType);
return this.eventTypesRepository.deleteEventType(eventTypeId);
}
checkUserOwnsEventType(userId: number, eventType: Pick<EventType, "id" | "userId">) {
if (userId !== eventType.userId) {
throw new ForbiddenException(`User with ID=${userId} does not own event type with ID=${eventType.id}`);
}
}
}
@@ -0,0 +1,50 @@
import { Injectable } from "@nestjs/common";
import {
transformApiEventTypeBookingFields,
transformApiEventTypeLocations,
} from "@calcom/platform-libraries-0.0.13";
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
@Injectable()
export class InputEventTypesService_2024_06_14 {
transformInputCreateEventType(inputEventType: CreateEventTypeInput_2024_06_14) {
const defaultLocations: CreateEventTypeInput_2024_06_14["locations"] = [
{
type: "integration",
integration: "cal-video",
},
];
const { lengthInMinutes, locations, bookingFields, ...rest } = inputEventType;
const eventType = {
...rest,
length: lengthInMinutes,
locations: this.transformInputLocations(locations || defaultLocations),
bookingFields: this.transformInputBookingFields(bookingFields),
};
return eventType;
}
transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14) {
const { locations, bookingFields, ...rest } = inputEventType;
const eventType = {
...rest,
locations: locations ? this.transformInputLocations(locations) : undefined,
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
};
return eventType;
}
transformInputLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) {
return transformApiEventTypeLocations(inputLocations);
}
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
return transformApiEventTypeBookingFields(inputBookingFields);
}
}
@@ -0,0 +1,150 @@
import { Injectable } from "@nestjs/common";
import type { EventType, User, Schedule } from "@prisma/client";
import {
EventTypeMetaDataSchema,
userMetadata,
getResponseEventTypeLocations,
getResponseEventTypeBookingFields,
parseRecurringEvent,
TransformedLocationsSchema,
BookingFieldsSchema,
} from "@calcom/platform-libraries-0.0.13";
type EventTypeRelations = { users: User[]; schedule: Schedule | null };
type DatabaseEventType = EventType & EventTypeRelations;
type Input = Pick<
DatabaseEventType,
| "id"
| "length"
| "title"
| "description"
| "disableGuests"
| "slotInterval"
| "minimumBookingNotice"
| "beforeEventBuffer"
| "afterEventBuffer"
| "slug"
| "schedulingType"
| "requiresConfirmation"
| "price"
| "currency"
| "lockTimeZoneToggleOnBookingPage"
| "seatsPerTimeSlot"
| "forwardParamsSuccessRedirect"
| "successRedirectUrl"
| "seatsShowAvailabilityCount"
| "isInstantEvent"
| "locations"
| "bookingFields"
| "recurringEvent"
| "metadata"
| "users"
| "schedule"
>;
@Injectable()
export class OutputEventTypesService_2024_06_14 {
async getResponseEventType(ownerId: number, databaseEventType: Input) {
const {
id,
length,
title,
description,
disableGuests,
slotInterval,
minimumBookingNotice,
beforeEventBuffer,
afterEventBuffer,
slug,
schedulingType,
requiresConfirmation,
price,
currency,
lockTimeZoneToggleOnBookingPage,
seatsPerTimeSlot,
forwardParamsSuccessRedirect,
successRedirectUrl,
seatsShowAvailabilityCount,
isInstantEvent,
} = databaseEventType;
const locations = this.transformLocations(databaseEventType.locations);
const bookingFields = this.transformBookingFields(databaseEventType.bookingFields);
const recurringEvent = this.transformRecurringEvent(databaseEventType.recurringEvent);
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
const schedule = await this.getSchedule(databaseEventType);
const users = this.transformUsers(databaseEventType.users);
return {
id,
ownerId,
lengthInMinutes: length,
title,
slug,
description: description || "",
locations,
bookingFields,
recurringEvent,
disableGuests,
slotInterval,
minimumBookingNotice,
beforeEventBuffer,
afterEventBuffer,
schedulingType,
metadata,
requiresConfirmation,
price,
currency,
lockTimeZoneToggleOnBookingPage,
seatsPerTimeSlot,
forwardParamsSuccessRedirect,
successRedirectUrl,
seatsShowAvailabilityCount,
isInstantEvent,
users,
schedule,
};
}
transformLocations(locations: any) {
if (!locations) return [];
return getResponseEventTypeLocations(TransformedLocationsSchema.parse(locations));
}
transformBookingFields(inputBookingFields: any) {
if (!inputBookingFields) return [];
return getResponseEventTypeBookingFields(BookingFieldsSchema.parse(inputBookingFields));
}
transformRecurringEvent(recurringEvent: any) {
if (!recurringEvent) return null;
return parseRecurringEvent(recurringEvent);
}
transformMetadata(metadata: any) {
if (!metadata) return {};
return EventTypeMetaDataSchema.parse(metadata);
}
async getSchedule(databaseEventType: Input) {
return databaseEventType.schedule || null;
}
transformUsers(users: User[]) {
return users.map((user) => {
const metadata = user.metadata ? userMetadata.parse(user.metadata) : {};
return {
id: user.id,
name: user.name,
username: user.username,
avatarUrl: user.avatarUrl,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
weekStart: user.weekStart,
metadata: metadata || {},
};
});
}
}
+10 -55
View File
@@ -2,10 +2,11 @@ import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GcalAuthUrlOutput } from "@/ee/gcal/outputs/auth-url.output";
import { GcalCheckOutput } from "@/ee/gcal/outputs/check.output";
import { GcalSaveRedirectOutput } from "@/ee/gcal/outputs/save-redirect.output";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { GCalService } from "@/modules/apps/services/gcal.service";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
@@ -40,8 +41,8 @@ const CALENDAR_SCOPES = [
// Controller for the GCalConnect Atom
@Controller({
path: "/gcal",
version: "2",
path: "/v2/gcal",
version: API_VERSIONS_VALUES,
})
@DocsTags("Google Calendar")
export class GcalController {
@@ -60,7 +61,7 @@ export class GcalController {
@Get("/oauth/auth-url")
@HttpCode(HttpStatus.OK)
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
async redirect(
@Headers("Authorization") authorization: string,
@Req() req: Request
@@ -81,61 +82,15 @@ export class GcalController {
@Redirect(undefined, 301)
@HttpCode(HttpStatus.OK)
async save(@Query("state") state: string, @Query("code") code: string): Promise<GcalSaveRedirectOutput> {
const stateParams = new URLSearchParams(state);
const { accessToken, origin } = z
.object({ accessToken: z.string(), origin: z.string() })
.parse({ accessToken: stateParams.get("accessToken"), origin: stateParams.get("origin") });
// User chose not to authorize your app or didn't authorize your app
// redirect directly without oauth code
if (!code) {
return { url: origin };
}
const parsedCode = z.string().parse(code);
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
if (!ownerId) {
throw new UnauthorizedException("Invalid Access token.");
}
const oAuth2Client = await this.gcalService.getOAuthClient(this.redirectUri);
const token = await oAuth2Client.getToken(parsedCode);
// Google oAuth Credentials are stored in token.tokens
const key = token.tokens;
const credential = await this.credentialRepository.createAppCredential(
GOOGLE_CALENDAR_TYPE,
key as Prisma.InputJsonValue,
ownerId
);
oAuth2Client.setCredentials(key);
const calendar = google.calendar({
version: "v3",
auth: oAuth2Client,
});
const cals = await calendar.calendarList.list({ fields: "items(id,summary,primary,accessRole)" });
const primaryCal = cals.data.items?.find((cal) => cal.primary);
if (primaryCal?.id) {
await this.selectedCalendarsRepository.createSelectedCalendar(
primaryCal.id,
credential.id,
ownerId,
GOOGLE_CALENDAR_TYPE
);
}
return { url: origin };
const url = new URL(this.config.get("api.url") + "/calendars/google/save");
url.searchParams.append("code", code);
url.searchParams.append("state", state);
return { url: url.href };
}
@Get("/check")
@HttpCode(HttpStatus.OK)
@UseGuards(AccessTokenGuard, PermissionsGuard)
@UseGuards(ApiAuthGuard, PermissionsGuard)
@Permissions([APPS_READ])
async check(@GetUser("id") userId: number): Promise<GcalCheckOutput> {
const gcalCredentials = await this.credentialRepository.getByTypeAndUserId("google_calendar", userId);
@@ -1,8 +1,7 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { SchedulesModule } from "@/ee/schedules/schedules.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
@@ -14,7 +13,7 @@ import { User } from "@prisma/client";
import * as request from "supertest";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { UserResponse } from "@calcom/platform-types";
@@ -31,17 +30,10 @@ describe("Me Endpoints", () => {
let user: User;
beforeAll(async () => {
const moduleRef = await withAccessTokenAuth(
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [
AppModule,
PrismaModule,
AvailabilitiesModule,
UsersModule,
TokensModule,
SchedulesModule,
],
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_04_15],
})
)
.overrideGuard(PermissionsGuard)
@@ -118,13 +110,13 @@ describe("Me Endpoints", () => {
});
it("should not update user associated with access token given invalid time format", async () => {
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 };
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 as any };
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
});
it("should not update user associated with access token given invalid week start", async () => {
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" };
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" as any };
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
});
+7 -6
View File
@@ -1,9 +1,10 @@
import { GetMeOutput } from "@/ee/me/outputs/get-me.output";
import { UpdateMeOutput } from "@/ee/me/outputs/update-me.output";
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
@@ -14,15 +15,15 @@ import { PROFILE_READ, PROFILE_WRITE, SUCCESS_STATUS } from "@calcom/platform-co
import { userSchemaResponse } from "@calcom/platform-types";
@Controller({
path: "/me",
version: "2",
path: "/v2/me",
version: API_VERSIONS_VALUES,
})
@UseGuards(AccessTokenGuard, PermissionsGuard)
@UseGuards(ApiAuthGuard, PermissionsGuard)
@DocsTags("Me")
export class MeController {
constructor(
private readonly usersRepository: UsersRepository,
private readonly schedulesRepository: SchedulesService
private readonly schedulesRepository: SchedulesService_2024_04_15
) {}
@Get("/")
+2 -2
View File
@@ -1,11 +1,11 @@
import { MeController } from "@/ee/me/me.controller";
import { SchedulesModule } from "@/ee/schedules/schedules.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [UsersModule, SchedulesModule, TokensModule],
imports: [UsersModule, SchedulesModule_2024_04_15, TokensModule],
controllers: [MeController],
})
export class MeModule {}
@@ -1,10 +1,12 @@
import { BookingsModule } from "@/ee/bookings/bookings.module";
import { CalendarsModule } from "@/ee/calendars/calendars.module";
import { EventTypesModule } from "@/ee/event-types/event-types.module";
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 { GcalModule } from "@/ee/gcal/gcal.module";
import { MeModule } from "@/ee/me/me.module";
import { ProviderModule } from "@/ee/provider/provider.module";
import { SchedulesModule } from "@/ee/schedules/schedules.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
import { SlotsModule } from "@/modules/slots/slots.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
import { Module } from "@nestjs/common";
@@ -13,9 +15,11 @@ import { Module } from "@nestjs/common";
imports: [
GcalModule,
ProviderModule,
SchedulesModule,
SchedulesModule_2024_04_15,
SchedulesModule_2024_06_11,
MeModule,
EventTypesModule,
EventTypesModule_2024_04_15,
EventTypesModule_2024_06_14,
CalendarsModule,
BookingsModule,
SlotsModule,
@@ -1,7 +1,8 @@
import { ProviderVerifyAccessTokenOutput } from "@/ee/provider/outputs/verify-access-token.output";
import { ProviderVerifyClientOutput } from "@/ee/provider/outputs/verify-client.output";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
@@ -20,8 +21,8 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@Controller({
path: "/provider",
version: "2",
path: "/v2/provider",
version: API_VERSIONS_VALUES,
})
@DocsTags("Cal provider")
export class CalProviderController {
@@ -44,7 +45,7 @@ export class CalProviderController {
@Get("/:clientId/access-token")
@HttpCode(HttpStatus.OK)
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
async verifyAccessToken(
@Param("clientId") clientId: string,
@GetUser() user: UserWithProfile
@@ -1,16 +0,0 @@
import { SchedulesController } from "@/ee/schedules/controllers/schedules.controller";
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, AvailabilitiesModule, UsersModule, TokensModule],
providers: [SchedulesRepository, SchedulesService],
controllers: [SchedulesController],
exports: [SchedulesService, SchedulesRepository],
})
export class SchedulesModule {}
@@ -1,12 +1,11 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output";
import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output";
import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output";
import { SchedulesModule } from "@/ee/schedules/schedules.module";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { CreateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/create-schedule.output";
import { GetSchedulesOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-schedules.output";
import { UpdateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/update-schedule.output";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
@@ -17,10 +16,10 @@ import { User } from "@prisma/client";
import * as request from "supertest";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { UpdateScheduleInput } from "@calcom/platform-types";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_04_15 } from "@calcom/platform-constants";
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
describe("Schedules Endpoints", () => {
describe("User Authentication", () => {
@@ -32,23 +31,16 @@ describe("Schedules Endpoints", () => {
const userEmail = "schedules-controller-e2e@api.com";
let user: User;
let createdSchedule: CreateScheduleOutput["data"];
let createdSchedule: CreateScheduleOutput_2024_04_15["data"];
const defaultAvailabilityDays = [1, 2, 3, 4, 5];
const defaultAvailabilityStartTime = "1970-01-01T09:00:00.000Z";
const defaultAvailabilityEndTime = "1970-01-01T17:00:00.000Z";
beforeAll(async () => {
const moduleRef = await withAccessTokenAuth(
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [
AppModule,
PrismaModule,
AvailabilitiesModule,
UsersModule,
TokensModule,
SchedulesModule,
],
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_04_15],
})
)
.overrideGuard(PermissionsGuard)
@@ -79,7 +71,7 @@ describe("Schedules Endpoints", () => {
const scheduleTimeZone = "Europe/Rome";
const isDefault = true;
const body: CreateScheduleInput = {
const body: CreateScheduleInput_2024_04_15 = {
name: scheduleName,
timeZone: scheduleTimeZone,
isDefault,
@@ -87,10 +79,11 @@ describe("Schedules Endpoints", () => {
return request(app.getHttpServer())
.post("/api/v2/schedules")
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
.send(body)
.expect(201)
.then(async (response) => {
const responseData: CreateScheduleOutput = response.body;
const responseData: CreateScheduleOutput_2024_04_15 = response.body;
expect(responseData.status).toEqual(SUCCESS_STATUS);
expect(responseData.data).toBeDefined();
expect(responseData.data.isDefault).toEqual(isDefault);
@@ -117,7 +110,7 @@ describe("Schedules Endpoints", () => {
.get("/api/v2/schedules/default")
.expect(200)
.then(async (response) => {
const responseData: CreateScheduleOutput = response.body;
const responseData: CreateScheduleOutput_2024_04_15 = response.body;
expect(responseData.status).toEqual(SUCCESS_STATUS);
expect(responseData.data).toBeDefined();
expect(responseData.data.id).toEqual(createdSchedule.id);
@@ -137,7 +130,7 @@ describe("Schedules Endpoints", () => {
.get(`/api/v2/schedules`)
.expect(200)
.then((response) => {
const responseData: GetSchedulesOutput = response.body;
const responseData: GetSchedulesOutput_2024_04_15 = response.body;
expect(responseData.status).toEqual(SUCCESS_STATUS);
expect(responseData.data).toBeDefined();
expect(responseData.data?.[0].id).toEqual(createdSchedule.id);
@@ -155,16 +148,17 @@ describe("Schedules Endpoints", () => {
it("should update schedule name", async () => {
const newScheduleName = "new-schedule-name";
const body: UpdateScheduleInput = {
const body: UpdateScheduleInput_2024_04_15 = {
name: newScheduleName,
};
return request(app.getHttpServer())
.patch(`/api/v2/schedules/${createdSchedule.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
.send(body)
.expect(200)
.then((response: any) => {
const responseData: UpdateScheduleOutput = response.body;
const responseData: UpdateScheduleOutput_2024_04_15 = response.body;
expect(responseData.status).toEqual(SUCCESS_STATUS);
expect(responseData.data).toBeDefined();
expect(responseData.data.schedule.name).toEqual(newScheduleName);
@@ -1,13 +1,14 @@
import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output";
import { DeleteScheduleOutput } from "@/ee/schedules/outputs/delete-schedule.output";
import { GetDefaultScheduleOutput } from "@/ee/schedules/outputs/get-default-schedule.output";
import { GetScheduleOutput } from "@/ee/schedules/outputs/get-schedule.output";
import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output";
import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output";
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
import { CreateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/create-schedule.output";
import { DeleteScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/delete-schedule.output";
import { GetDefaultScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-default-schedule.output";
import { GetScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-schedule.output";
import { GetSchedulesOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-schedules.output";
import { UpdateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/update-schedule.output";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { VERSION_2024_04_15_VALUE } from "@/lib/api-versions";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
@@ -26,25 +27,25 @@ import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
import { Throttle } from "@nestjs/throttler";
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { UpdateScheduleInput } from "@calcom/platform-types";
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
import { CreateScheduleInput } from "../inputs/create-schedule.input";
import { CreateScheduleInput_2024_04_15 } from "../inputs/create-schedule.input";
@Controller({
path: "schedules",
version: "2",
path: "/v2/schedules",
version: VERSION_2024_04_15_VALUE,
})
@UseGuards(AccessTokenGuard, PermissionsGuard)
@UseGuards(ApiAuthGuard, PermissionsGuard)
@DocsTags("Schedules")
export class SchedulesController {
constructor(private readonly schedulesService: SchedulesService) {}
export class SchedulesController_2024_04_15 {
constructor(private readonly schedulesService: SchedulesService_2024_04_15) {}
@Post("/")
@Permissions([SCHEDULE_WRITE])
async createSchedule(
@GetUser() user: UserWithProfile,
@Body() bodySchedule: CreateScheduleInput
): Promise<CreateScheduleOutput> {
@Body() bodySchedule: CreateScheduleInput_2024_04_15
): Promise<CreateScheduleOutput_2024_04_15> {
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
@@ -56,8 +57,14 @@ export class SchedulesController {
@Get("/default")
@Permissions([SCHEDULE_READ])
@ApiResponse({ status: 200, description: "Returns the default schedule", type: GetDefaultScheduleOutput })
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetDefaultScheduleOutput | null> {
@ApiResponse({
status: 200,
description: "Returns the default schedule",
type: GetDefaultScheduleOutput_2024_04_15,
})
async getDefaultSchedule(
@GetUser() user: UserWithProfile
): Promise<GetDefaultScheduleOutput_2024_04_15 | null> {
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
const scheduleFormatted = schedule
? await this.schedulesService.formatScheduleForAtom(user, schedule)
@@ -75,7 +82,7 @@ export class SchedulesController {
async getSchedule(
@GetUser() user: UserWithProfile,
@Param("scheduleId") scheduleId: number
): Promise<GetScheduleOutput> {
): Promise<GetScheduleOutput_2024_04_15> {
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
@@ -87,7 +94,7 @@ export class SchedulesController {
@Get("/")
@Permissions([SCHEDULE_READ])
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput> {
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_04_15> {
const schedules = await this.schedulesService.getUserSchedules(user.id);
const schedulesFormatted = await this.schedulesService.formatSchedulesForAtom(user, schedules);
@@ -102,9 +109,9 @@ export class SchedulesController {
@Permissions([SCHEDULE_WRITE])
async updateSchedule(
@GetUser() user: UserWithProfile,
@Body() bodySchedule: UpdateScheduleInput,
@Body() bodySchedule: UpdateScheduleInput_2024_04_15,
@Param("scheduleId") scheduleId: string
): Promise<UpdateScheduleOutput> {
): Promise<UpdateScheduleOutput_2024_04_15> {
const updatedSchedule = await this.schedulesService.updateUserSchedule(
user,
Number(scheduleId),
@@ -123,7 +130,7 @@ export class SchedulesController {
async deleteSchedule(
@GetUser("id") userId: number,
@Param("scheduleId") scheduleId: number
): Promise<DeleteScheduleOutput> {
): Promise<DeleteScheduleOutput_2024_04_15> {
await this.schedulesService.deleteUserSchedule(userId, scheduleId);
return {
@@ -3,7 +3,7 @@ import { ApiProperty } from "@nestjs/swagger";
import { Transform, TransformFnParams } from "class-transformer";
import { IsArray, IsDate, IsNumber } from "class-validator";
export class CreateAvailabilityInput {
export class CreateAvailabilityInput_2024_04_15 {
@IsArray()
@IsNumber({}, { each: true })
@ApiProperty({ example: [1, 2] })
@@ -1,8 +1,8 @@
import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input";
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
import { Type } from "class-transformer";
import { IsArray, IsBoolean, IsTimeZone, IsOptional, IsString, ValidateNested } from "class-validator";
export class CreateScheduleInput {
export class CreateScheduleInput_2024_04_15 {
@IsString()
name!: string;
@@ -11,9 +11,9 @@ export class CreateScheduleInput {
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateAvailabilityInput)
@Type(() => CreateAvailabilityInput_2024_04_15)
@IsOptional()
availabilities?: CreateAvailabilityInput[];
availabilities?: CreateAvailabilityInput_2024_04_15[];
@IsBoolean()
isDefault!: boolean;
@@ -1,11 +1,11 @@
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.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 CreateScheduleOutput {
export class CreateScheduleOutput_2024_04_15 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@@ -3,7 +3,7 @@ import { IsEnum } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class DeleteScheduleOutput {
export class DeleteScheduleOutput_2024_04_15 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;

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