Compare commits

..
251 Commits
Author SHA1 Message Date
Omar López 89c08868f6 chore: v4.3.4 (#16010)
release.yml / release (push) Canceled after 0s
2024-07-31 21:01:49 +00:00
Omar López ebab3b701d revert: "fix: meeting link is not updated and broken ui in edit location dialog" (#16011) 2024-07-31 23:16:45 +03:00
Anik Dhabal BabuandKeith Williams 9ffa4b8477 fix: meeting link is not updated and broken ui in edit location dialog (#15503)
* fix: meeting link is not updated

* fix: broken ui in edit location dialog

* Update editLocation.handler.ts

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-31 19:05:20 +00:00
Amit Sharma c656063b06 fix: incorrect variable (#16002) 2024-07-31 13:48:32 +00:00
Hariom BalharaandUdit Takkar 5b7f2a6ffc fix: Reschedule URL for an org migrated/moved user's booking. (#15736)
* Fix reschedule URl

* Add e2e for org

* Add team booking reschedule test

* Add unit tests

* Remove unnecessary select

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-07-31 14:25:46 +02:00
Imamuzzaki Abu SalamandAmit Sharma 63c7e4b55a test: add tests for components/editor (#15938)
* test: add VariableNode test suite

* test: add ToolbarPlugin test suite

* test: add AutoLinkPlugin test suite

* test: add AddVariablesDropdown test suite

* test: add Editor test suite

* chore: Remove unnecessary comments in ToolbarPlugin.test.tsx

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-07-31 14:25:00 +02:00
Carina WollendorferandCarinaWolli 24e5226e3e fix: issues with verified emails in workflow (#15965)
* fix verified emails

* review feedback

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-07-31 11:58:14 +00:00
sean-brydon 9f43c5f1e7 add row on click if user can edit (#15998) 2024-07-31 11:12:48 +00:00
MohitandAnik Dhabal Babu b48e3c7bb4 fix: alert message is not positioned properly (#15995)
* fix: correct button functionality in alert component

* add: bottom margin to alert component

* revert alert comp changes

* small update

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-31 00:23:11 +03:00
Morgan a561b805fd chore: add doc + test reschedule booking api v1 (#15907)
* chore: add doc + test reschedule booking api v1

* fixup! Merge branch 'main' into fix-reschedule-booking-api-v1
2024-07-31 00:22:25 +03:00
VijayandAnik Dhabal Babu d21c91c519 fix: #15989 disable country prefill in phone input (#15991)
* default country disabled in phone input

* Update PhoneInput.tsx

* update event-types e2e

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-31 00:20:37 +03:00
Lauris SkraucisandMorgan 41e565fcf8 refactor: platform org event types (#15961)
* chore: platform-libraries export updateNewTeamMemberEventTypes

* chore: platform-libraries export updateNewTeamMemberEventTypes

* chore: v2 api bump platform-libraries to 0.0.22

* fix: add created managed user to assignAllMembers: true team event-types

* fix: add created organizations user to assignAllMembers: true team event-types

* refactor: organization teams chose if team creator is auto-accepted

* refactor: managed users have only 2 default event types with video

* refactor: lowercase schedulingType for creating team event-types

* refactor: lowercase schedulingType for creating team event-types

* feat: separate platform teams by OAuth client

* swagger regenerate

* refactor: delete platform team when oauth client deleted

* revert: keep 4 default event-types

* fix: tests && merge of main into this branch

* fix: typecheck

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-07-30 22:23:31 +03:00
Lauris SkraucisandMorgan 2f946fc5da feat: platform team event-types (#15928)
* fix: dont check OrganizationSettings.orgAutoAcceptEmail uniqueness for platform teams

* feat: display organizationId in OAuthClientCard

* feat: ApiAuthStrategy handle oauth credentials

* refactor: clean up ApiAuthStrategy test

* refactor: use ApiAuthGuard in OauthClientsUsersController and OAuthFlowController

* fix: copying org id

* refactor: more specific error message when api auth

* refactor: auto accept team creator membership

* feat: useTeamEventType and useTeamEventTypes hooks

* refactor: make team event-types public & searchable by eventSlug

* feat: include host name in team event-types hosts response

* fix: isFixed=true by default for COLLECTIVE event hosts

* fix: useTeamEventType eventSlug access

* feat: BookerPlatformWrapper enable team events by exposting orgId and teamId props

* refactor: provide orgId in atoms context

* refactor: use orgId from context in team event-types hooks

* refactor: return teams in useMe

* chore: examples app teams setup

* Revert "refactor: return teams in useMe"

This reverts commit de992ddc9af6ee9a2111938069f5b9c34cc2d8ea.

* Revert "chore: examples app teams setup"

This reverts commit 0766aa21acc25efa2361d38c3f87ddba773a0245.

* feat: useTeams hook

* chore: setup examples app with team event-type

* fix: small fixes

* swagger

* Revert "refactor: provide orgId in atoms context"

This reverts commit f053a498ee6f8fa8ece5ec8d8630c59eda8873e3.

* feat: orgId in atoms context

* feat: PlatformBilling guard

* chore: delete test of the deleted oauth-client-credentials.guard

* refactor: org event-types collective events isFixed always true and priority medium

* refactor: org event-types COLLECTIVE response ignore isFixed and priority as they are same

* fix: organizations event-types e2e

* fix: billing guard

* refactor: tests cleanup

* fix: platform plan guard spec

* refactor: e2e test cleanup

* seed error if not team

* refactor: rename authenticateApiKey to authenticateBearerToken

* refactor: transforming response hosts

* refactor: rename findUniqueByMatchingAutoAcceptEmail to findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail

* refactor: rename findUniqueByMatchingAutoAcceptEmail to findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-07-30 16:58:39 +02:00
06bb7055ac fix: added translations for connect atoms (#15969)
* fix: added translations for connect atoms

* fix: already spelling

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-30 14:34:05 +00:00
Peer Richelsen 1a8492b9c2 chore: fixed a few destructive buttons (#15990) 2024-07-30 16:24:11 +03:00
b8ba95dbb0 feat: min max chars for long text (#15691)
* feat: min max chars for long text

* refactor: remove console log

* refactor: revert redundant change

* refactor: review changes

* fix: min max validation

* fix: fix typo

* refactor: update comments

* refactor: review changes

* refactor: review changes

* test: Add test

* Make maxLength more configurable per field type

* Reduce maxLength to 1000

* Update update-event-type.input.ts

* revert changes in main

* Update update-event-type.input.ts

* refactor get-event-type-public.output.ts

* add e2e tests

* add min max char common.json

* Update FormBuilder.tsx

* refactor changes

* Update fieldTypes.ts

* Update schema.ts

* add min max length in api-request

* Update booking-fields.input.ts

* fix e2e tests

* move min-max-char tests to manage-booking-questions.e2e.ts

* remove test

* reorder imports to fix lint

* fix lint

* fix e2e tests

* Abstract out SupportsLengthcheck type Field

* Revert newline change

* Improve the schema validation a bit and fix undefined in the message issue

* revert: v2 API changes

---------

Co-authored-by: Ayush Mhetre <mhetreayush1719@gmail.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
2024-07-30 12:21:27 +00:00
40df4cf511 fix: Add strict checks for email to create an organization (#15976)
* Verify email first

* Only allow company emails for org creation

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-07-30 11:52:10 +00:00
58cabe974c fix: Add signup disabled flag check on signup route (#15844)
* fix: Add signup disabled flag check on signup api

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* feat: Implement a feature flag check trcp middleware

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* Discard changes to packages/trpc/server/middlewares/featureFlagMiddleware.ts

* Discard changes to packages/trpc/server/routers/viewer/organizations/_router.tsx

---------

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-07-30 11:26:20 +00:00
Udit TakkarandJoe Au-Yeung 199d95b35f feat: cal ai templates (#15391)
* chore: save progress

* feat: add schema and fieds

* chore: update code

* chore: save

* refactor: code and type error

* feat: use facade + command design pattern

* chore: missing param

* feat: add custom template feature

* refactor: code

* chore: move the default prompt

* fix: type err

* fix: code and ux improvements

* chore: change default template

* fix: ui and typo

* fix: add validation check for my phone number

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-07-30 13:09:12 +02:00
VijayandAnik Dhabal Babu c6febad364 fix: #15968 update to show booking question variables in Available variables (#15981)
* update to show booking question variables in Available variables

* small fix

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-30 08:46:44 +00:00
Morgan 0ddcc068ce fix: bullMq redis connection string apiv2 (#15970)
* fix: bullMq redis connection string apiv2

* fixup! fix: bullMq redis connection string apiv2
2024-07-30 08:27:27 +00:00
Anik Dhabal BabuandSomay Chauhan 3ff3a97c78 feat: ability to provide a reason for event rejection (from email) and some changes (#15098)
* feat: ability to provide reason for event rejection from email

* update

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-07-30 09:35:02 +02:00
MorganandKeith Williams baaa1fa71e chore: add global rate limiting to prevent abuse apiv2 (#15971)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-30 06:34:31 +00:00
27d331ad2d fix: #15967 Show better error when wrong variable is used in custom event name (#15974)
* update to show exact err msg with custom event name

* updated tests for validateCustomEventName

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-29 20:23:53 +03:00
Joe Au-YeungandAnik Dhabal Babu 9a1a86ed3a test: Delete credential endpoint (#15322)
* Init

* Add mock trpc context

* Intro mockDatabaseClient

* Introduce mockDatabaseClient class

* Add delete video app test

* Add calendar test

* Remove unused func from bookingScenario

* Remove console.log

* Add app repository

* Add createMany method to event type repository

* Remove instance of MockDatabaseClient from video test

* Add destination calendar repository

* Remove instances of MockDatabaseClient

* abstract logic to own handler

* Remove dev dependency

* Clean up

* Type fix

* Pull yarn.lock from main

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-29 11:38:58 -04:00
Keith Williams 7902c6157d fix: Broken GitHub workflows (#15979) 2024-07-29 17:35:52 +03:00
sean-brydonandHariom Balhara 92d0fc6eb4 fix: auto accept (#15975)
* fix: auto accept

* Update packages/lib/server/repository/organization.ts

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2024-07-29 14:18:55 +00:00
Carina WollendorferandCarinaWolli 4d0b417925 fix: type in webhook payload (#15912)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-07-29 13:48:45 +00:00
Keith Williams 9947b5ec85 fix: Next.js bundle analysis (#15903)
* fix: Next.js bundle analysis

* Added the main checkout back
2024-07-29 13:47:50 +00:00
fa06f38d47 fix: refresh connect atoms on token change (#15933)
* added dropdown

* change comment

* fix: added types

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-29 16:31:31 +03:00
Lauris SkraucisandKeith Williams 99a6b1c7eb refactor: v1 orgs unlimited teams (#15908)
* refactor: v1 orgs unlimited teams

* fix: response message ownerId

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-29 16:31:18 +03:00
e3861ad22a feat: No-show bookings - do not count towards a member's round robin assignment (#15323)
* feat: No-show bookings - do not count towards a member's round robin assignment

* filter no show organizer

* remove logs

* fix: filter bookings where host attendee is no show

* fix: use not condition for noShow

* chore: test

* add default false for noShowHost

* Revert "chore: test"

This reverts commit c914a8fc4a14ca9c7d8c4a08ff8b0f463789171c.

* chore: integration test

* fix: noShowHost not working for null

* Update packages/lib/server/getLuckyUser.ts

* chore: update test description

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: gh0st91848 <gh0st91848@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2024-07-29 16:05:03 +03:00
Hariom BalharaandKeith Williams 37caa57798 fix: Add CORP and COEP headers to embedded pages (#15275)
* Add CORP headers

* Improve test

* Add unit tests as well

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-29 11:54:31 +02:00
6e66f7f8f8 fix: Assignment reminder modal appears when deleting a team event type (#15544)
* fix: Assignment reminder modal appears when deleting a team event type

* update

* Update EventTypeSingleLayout.tsx

* Update event-types-single-view.tsx

* update

* Early return https://handbook.cal.com/engineering/best-practices/prefer-early-returns

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-07-29 09:52:03 +00:00
Hichem FantarandHariom Balhara 62dd5ea95b feat: Add title attribute to iframe in embed.ts (#15944)
* feat: Add title attribute to iframe in embed.ts

* Update packages/embeds/embed-core/src/embed.ts

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2024-07-29 08:14:13 +00:00
Johannes Maendle 365dfe9178 i18n: update translations for "by proceeding, ..." (#15921) 2024-07-29 06:04:33 +00:00
Hichem FantarandKeith Williams 4d89da99a7 feat: pwa ready and add shortcut for quick access (#15957)
* chore: Update site.webmanifest with additional display properties

* chore: Update site.webmanifest with additional display properties

* Update apps/web/public/site.webmanifest

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-29 05:37:31 +00:00
Souptik DattaandKeith Williams 6954ad1ea5 fix: Change minimum number of teams required for org self-serve (#15955)
* fix: Change minimum number of teams required for org self-serve

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* refactor: Use a new constant for displaying org helper text dialogue

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* Update packages/lib/constants.ts

* Update turbo.json

---------

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-29 05:17:17 +00:00
Hichem Fantar 825573da8e feat: add transitions to more components (#15916)
* add transitions to more components

* chore: Remove duplicate class
2024-07-29 08:06:44 +03:00
2c517f4878 fix: getting an error when a team member tries to leave (#15925)
a team

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-07-29 07:21:56 +03:00
VijayandAnik Dhabal Babu 8508a6f1a0 fix: #15697 Assignment warning modal poping up ,even when hosts are added . (#15699)
* removed reduntand check on assignedusers

* update for managed eventtype

* formatted

* comments

* Updated comments

* extracted checkForEmptyAssignment and added tests

* updated comments

* updated to show dialog also when not yet saved

* update translations

* updated comments

* updated translation

* updated types

* chore: update naming and comments

* Delete apps/web/lib/checkForEmptyAssignment.ts

deleted since file was renamed

* Delete apps/web/test/lib/checkForEmptyAssignment.test.ts

deleted as the file is renamed to be more descriptive

* update to do teamcheck before

* chore: revert name to CheckForEmptyAssignment

* updated check conditions

* updated useEffect dependencies

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-29 07:02:59 +03:00
db03e9db20 style: update text area (#15934)
* chore: update text-editor input

* chore: update text-editor input

* final update

* Update Editor.tsx

---------

Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-29 06:51:53 +03:00
Peer Richelsen e6a16f5b5c chore: Update feature_request.md (#15926)
* Update feature_request.md

* Update feature_request.md
2024-07-26 12:49:19 +00:00
b94aa603c5 chore: Add missing comments for self hosted billing (#15923)
* 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

* Inital setup

* Fix verifyApi Key tests

* Implement tests for LicenseKeyService

* Restore old checkLicense code

* check license key first

* fix imports

* fix url in tests. Implement prisma extention to track creation

* Use data.status for licenseCheck

* extract fetch to class

* improve error

* [wip] per user tracking

* Remove new URL as it was breaking search params

* Use client extention in customPrismaClient

* Remove log

* Remove license required from creation form as itll fail

* Fix annual price calculation

* add api route to .env.example and constants

* re-word instructions in .env.example

* chore: feedback

* Update packages/features/ee/common/server/LicenseKeyService.ts

* Add IS_E2E_ENABLED constant + update tests

* Revert "Add IS_E2E_ENABLED constant + update tests"

This reverts commit d059b73f6272bc06fd2ab5ed505b6d52ea030a56.

* chore: tests fixes

* fixes: unit tests

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

* chore: add missing instructions

---------

Signed-off-by: zomars <zomars@me.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-07-26 10:10:46 +00:00
31e6b88951 fix: Fix teams helper image background color mode (#15918)
Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-26 10:25:39 +02:00
Alex van Andel fbff4db2a6 chore: Release v4.3.3 (#15924)
release.yml / release (push) Canceled after 0s
2024-07-26 09:00:00 +01:00
Udit Takkar ba61731689 fix: make max_participants optional (#15920) 2024-07-26 07:08:05 +00:00
sean-brydonandOmar López e16662e655 feat: usage base billed - license key check - move to private api (#15222)
Co-authored-by: Omar López <zomars@me.com>
2024-07-25 13:20:24 -07:00
sean-brydon 53c37a9ddf feat: refactor drawer component (#15892)
* Add sheet component

* refactor user edit sheet

* refactor availbility sheet edit

* Remove tremor raw sheet text

* remove accidently file commit

* fix overflow issue
2024-07-25 12:41:23 +01:00
Morgan 64ef2703a5 feat: org members can fetch their teams apiv2 (#15910)
* feat: org members can fetch their teams apiv2

* fixup! feat: org members can fetch their teams apiv2

* feat: org members can fetch their teams apiv2

* fixup! feat: org members can fetch their teams apiv2

* fixup! fixup! feat: org members can fetch their teams apiv2

* fixup! fixup! fixup! feat: org members can fetch their teams apiv2

* fixup! fixup! fixup! fixup! feat: org members can fetch their teams apiv2
2024-07-25 10:39:43 +00:00
Rajiv SahalandMorgan Vernay a997edd8db chore: update dtos for v2 calendars endpoint (#15904)
* init calendars input

* update request body input type

* fix incorrect naming

* update imprts

* fixup! update imprts

* fixup! fixup! update imprts

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-07-25 08:49:18 +00:00
Peer Richelsen bd73eba528 chore: Update config.yml (#15900) 2024-07-25 09:35:33 +02:00
0d53c2069e feat: team owner/admin, be able to confirm/reject "requires confirmation" team events (#15326)
* feat: eam owner/admin, be able to confirm/reject requir
es confirmation team events

* update

* addressed requested changes

* remove console.log

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-07-25 09:35:03 +02:00
Souptik Datta 3b559525a8 fix: Unable to invite a user to a team via username (#15868)
* fix: Fix invalid existing username DB query

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* fix: Support adding org team member using username

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* fix: Remove invite by username logic and fix labe to display only email

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* refactor: Remove username support completely from invitation-modal component

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

---------

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>
2024-07-25 07:04:44 +00:00
Peer Richelsen 33e1abf4a7 chore: fixed bug_report.yml (#15901) 2024-07-24 22:55:48 +00:00
059c6c37ba fix google calendar event not deleted on default calendar of cal event change (#14994)
Co-authored-by: Shaik-Sirajuddin <sirajudddinshaik30@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-07-24 15:34:41 +00:00
Peer Richelsen 14bc489d70 chore: Update bug_report.md (#15894)
* Update bug_report.md

* Apply suggestions from code review
2024-07-24 15:22:45 +00:00
Morgan e8b36d5b0b chore: platform api usage based billing using queue (#15085)
* chore: platform api usage based billing using queue

* fixup! chore: platform api usage based billing using queue

* fixup! fixup! chore: platform api usage based billing using queue

* fixup! Merge branch 'chore-platformapi-billing-queue' of github.com:calcom/cal.com into chore-platformapi-billing-queue

* fixup! fixup! Merge branch 'chore-platformapi-billing-queue' of github.com:calcom/cal.com into chore-platformapi-billing-queue

* fixup! Merge branch 'main' into chore-platformapi-billing-queue

* chore: platform billing by userId

* fix: fix tests requirement team subscirption

* fixup! Merge branch 'main' into chore-platformapi-billing-queue

* fixup! fixup! Merge branch 'main' into chore-platformapi-billing-queue

* fixup! Merge branch 'main' into chore-platformapi-billing-queue

* improve tests logs

* fix: silently close redis and queue

* fixup! Merge branch 'main' into chore-platformapi-billing-queue

* fixup! Merge branch 'main' into chore-platformapi-billing-queue

* fixup! fixup! Merge branch 'main' into chore-platformapi-billing-queue

* fix: upgrade libraries version
2024-07-24 17:10:22 +02:00
Saket kumar singh 5983c960a6 Fix: #15888 (#15890) 2024-07-24 14:03:31 +00:00
Souptik Dattaandunknown 40305b9c5a fix: Change conferencing app-store URL on event types and bookings pages (#15878)
* fix: Change conferencing app-store URL on event types and bookings pages

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* chore: Change conference app store help text

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* chore: Revert app install helper text key for all locales except en

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* delete key and text

---------

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
2024-07-24 14:02:13 +00:00
Lauris Skraucis dcc62c6db0 fix: CreateAvailabilityInput_2024_04_15 Cannot read properties of undefined (#15885) 2024-07-24 12:52:42 +00:00
Lucas Víctor Ferreira de AraújoandAnik Dhabal Babu 07f14e4e14 test: test cases for POST selected-calendars (#15873)
* test-selected_calendars

* Update _post.test.ts

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-24 14:29:09 +02:00
Keith Williams f940f6bd87 chore: Remove unit tests from running after crowd in (#15880)
* chore: Remove unit tests from running after crowd in

* Update permissions on all workflows

* Updated pr permissions

* Added actions:write to top-level pr

* Update .github/workflows/unit-tests.yml

* Removed the linting annotation
2024-07-24 15:18:11 +03:00
Keith Williams ca5dcfd1e8 chore: Add v2 API test suite (#15865)
* chore: Add v2 API test suite

* Changed oauth credentials guard to e2e spec

* Uploading e2e artifacts

* Removed commented line
2024-07-24 11:02:03 +02:00
Lauris Skraucis 324728d4ec refactor: v2 organizations event-types e2e tests (#15886) 2024-07-24 07:15:26 +00:00
Trillium S 2f7cc05cad fix: Update component order, props for Link in user-public-view (#15882) 2024-07-24 12:28:52 +05:30
Keith Williams 84be4dba8d chore: v4.3.2 (#15879)
release.yml / release (push) Canceled after 0s
2024-07-23 16:52:12 +02:00
Peer Richelsen 17655cd1c8 chore: updated feature_request.md template (#15877)
* Update feature_request.md

* Update .github/ISSUE_TEMPLATE/feature_request.md
2024-07-23 15:10:25 +02:00
Amit SharmaandSomay Chauhan 010f5349f6 feat: Link to rescheduled booking from the old one (#15211)
* feat: Link to rescheduled booking from the old one

* add original comment

* use fromReschedule

* fix: type check

* review updates

* Update common.json

* change booking page text

* Delete packages/features/bookings/lib/getRescheduledToBooking.tsx

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-07-23 12:39:50 +00:00
Lauris Skraucis 01dead9a1d refactor: auto-accept membership for v2 org team creator (#15866) 2024-07-23 12:34:33 +00:00
Peer Richelsen 900ea37b43 chore: updated bug_report.md template (#15876)
* Update bug_report.md

* Update .github/ISSUE_TEMPLATE/bug_report.md

* Update .github/ISSUE_TEMPLATE/bug_report.md

* Update .github/ISSUE_TEMPLATE/bug_report.md
2024-07-23 12:24:42 +00:00
Morgan c209c22b98 chore: add cleanup to api auth strategy e2e test apiv2 (#15875) 2024-07-23 12:22:46 +00:00
b8610dc2d1 test: Create 4 test cases to POST Attendees (#15856)
* Create 4 test cases to POST Attendees

* Adjusted commented tips on PR

* Adjusted isAdmin to isSystemWideAdmin

* Removed req.isAdmin

* Delete test-insights.csv

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-23 14:19:36 +02:00
Anik Dhabal BabuandAmit Sharma 28f84d3570 feat: skype app added (#14751)
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-07-23 09:39:34 +00:00
b46a902a3d feat: api endpoints for calendar settings atom (#15761)
* calendars repository

* add disconnect endpoint

* update platform libraries to include getCalendar fn

* cleanup

* fix typings

* update calendars service to include delete credentials handler

* update typings

* add missing return

* refactor delete calendar credentials handler

* update disconnect endpoint

* fix typing

* fixup

* add handlers to insert and remove selected calendar

* init selected calendars controller

* fix naming

* output response dto for deleting calendar credentials

* fix typing

* fix merge conflicts

* resolve merge conflicts

* fixup

* cleanup

* capitalize controller name

* include selected calendars controller in selected calendars module

* fix type error

* cleanup

* fixup

* add calendars repository

* custom hook for calendar credentials

* fix typing

* cleanup

* take input from query params instead of body in delete request

* custom hook to add selected calendar

* custom hook to remove selected calendar

* better naming

* fix typo

* fixup

* update input for delete calendar query params

* address PR feedback

* add method to check calendar credentials

* abstract logic to check calendar credentials in calendars service

* resolve module errors

* export custom hooks for calendar settings

* e2e for deleting calendar credentials endpoint

* fix typo

* set authorization header to calendars post

* e2e tests for selected calendars controller

* fix output typing

* better error messages

* restructuring

* fix imports

* remove unused not found exception

* fixup! remove unused not found exception

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-07-22 17:26:39 +00:00
Lauris Skraucis de2f51e104 feat: v2 API enable updating event type schedule (#15803)
* refactor: event-types CREATE, UPDATE accept scheduleId and return scheduleId

* chore: enable passing scheduleId to event-types create handler

* chore: use platform-libraries at version 0.0.20

* fix: remove team output schedule

* finish merge main into this branch

* fix: dont allow adding scheduleId to event-type if user does not own schedule
2024-07-22 20:09:54 +03:00
Hariom Balhara 35110892c1 fix: Fix React cal api namespaced usage (#15834)
* fix: Fix React cal api namespaced usage

* Update e2e
2024-07-22 12:01:05 +02:00
Hichem FantarandAnik Dhabal Babu c870df7027 chore: Add transition effects in UI components (#15841)
* chore: Add transition to hover effects in UI components

* chore: Add transition to hover effects in UI components

* Update ToggleGroup component transition

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-22 11:50:44 +02:00
Krishnendu SukumarandAnik Dhabal Babu a4b0fdba7b fix: Text in React Embed Code Becomes Invisible on Selection (#15835)
* fix: #15832

* fix: #15832

* Revert yarn.lock to its state before unintended changes

* Update EmbedTabs.tsx

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-22 09:26:13 +02:00
Punit Kumar Ojha b530d31b35 fix: added fix for email title for issue# 15848 (#15855) 2024-07-22 09:22:08 +02:00
Anik Dhabal Babu a69f9c5fa3 fix: broken ui of availability setup in onboarding (#15854) 2024-07-22 09:20:48 +02:00
73b8569a16 fix: added translations for et (#15762)
* added translations for et

* estonian support

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-22 08:42:07 +02:00
Hariom BalharaandUdit Takkar 5becc95fc4 feat: Add a fresh signup to matching organization after email verification (#15705)
* Support automatically adding user to organization

* Use Repository

* Fix unit test

* Code Improvements

* Fix type error

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-07-20 12:22:42 +05:30
SuPrabhuandsumantha a74522af3d fix: TimeZone select should show the selected timezone (#15838)
* bugfix - CAL4052 TimeZone select should show the selected timezone

* Pusheding the code for type - check failure

* Updating MenuPosition as absolute

---------

Co-authored-by: sumantha <sumantha-ullasa-prabhu@hpe.com>
2024-07-19 23:49:12 +05:30
Lauris Skraucis 8b23e41954 feat: v2 organizations event types endpoints (#15775)
* fix: event-types input service transform length property

* feat: create organizations event-types controller, service and repository

* feat: enable organizations event types

* fix: create team request user isOrgAdmin check

* refactor: hosts array required in team input and output

* chore: use platform-libraries at version 0.0.19

* tests: test organizations event-types

* feat: enable managed event-types

* chore: fix main merge into this branch

* feat: assign all team members for managed event-types

* refactor: gathering managed event-type users ids

* chore: include assignAllTeamMembers in team event response

* test: assign all team members db event

* chore: delete unused files

* docs: swagger create and update request body objects

* revert: event-types platform-libraries update

* refactor: organisations event-types endpoints accessible by TEAM_ADMIN and up

* fix: managed event types output hosts array empty

* fix: dont allow creating event-type for non-team member

* chore: delete unused variables

* tests: negative tests

* chore: finish main merge
2024-07-19 15:12:05 +00:00
Morgan 94873ea3cb fix: api-auth-strategy tests are e2e apiv2 (#15837) 2024-07-19 14:51:50 +00:00
0c0cac28fd fix: multiple getSchedule requests on email change in booker (#15616)
* fix: multiple getSchedule requests on email change in booker

* fix: use debounce for formEmail

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-19 12:30:41 +00:00
Richard Poelderl 769ca9002e fix(website): fix sitemap by adding prisma env var (#15827)
* update docs for `yarn.lock` management

* fix(website): Add `DATABASE_URL` as required environment variable to `website`
2024-07-19 11:56:37 +00:00
Morgan 03366d2337 feat: api v2 org team memberships endpoints (#15820)
* feat: api v2 org team memberships endpoints

* fixup! feat: api v2 org team memberships endpoints
2024-07-19 11:44:16 +00:00
159fe3a87d fix: system default theme for dashboard and booking page (#15291)
* fix: system default theme

* update

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
2024-07-19 13:42:21 +02:00
Morgan 5b97d3bf52 fix: revert libraries version event-types controller apiv2 (#15831) 2024-07-19 08:50:15 +00:00
Morgan 7cdecf57e0 fix: PATCH /me formatted timezone incorrectly (#15830) 2024-07-19 08:49:59 +00:00
Udit Takkar 1c8d0eda45 chore: update translation (#15818)
* chore: change title and description

* chore: update
2024-07-19 07:40:59 +00:00
7ee3f9409c feat: [CAL-3939] fix org banner (#15557)
* update documentation for proper hostile command

* add OrgBanner component and replace it in ee organization profile settings

* add width and height defaults, with option to override

* updated sizing with image loaded

* fix: add alt to OrgBanner in test

* fix: failing OrgBanner test, added neg test

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-07-19 05:30:50 +00:00
Keith Williams 0d1dd036eb revert: "chore: removed cal.ai email assistant (#15793)" (#15823) 2024-07-18 11:57:40 +00:00
Omar López 2af27170cd chore: v4.3.1 (#15817)
release.yml / release (push) Canceled after 0s
2024-07-18 13:09:30 +02:00
Joe Au-Yeungandzomars 218f6d6eaf feat: Salesforce - Disable creating new contacts (#15780)
Co-authored-by: zomars <zomars@me.com>
2024-07-18 10:50:42 +00:00
Hariom Balhara f0464e72c7 fix: Check URL (#15815) 2024-07-18 10:20:03 +00:00
Anik Dhabal BabuandPeer Richelsen 2ad3592eb3 chore: Introduce a new class WebAppURL that extends URL (#14982)
* chore: Introduce a new class WebAppURL that extends URL

* update

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-07-18 07:27:34 +00:00
Syed Ali ShahbazandMorgan Vernay 22fc47f69f feat: V2 Org Memberships (#15578)
* init

* --WIP

* WIP

* ...

* more wip

* doc

* address feedback --WIP

* further requested changes

* fix

* add missing parseIntPipe

* chore: code review comments + e2e tests

* fixup! chore: code review comments + e2e tests

* fixup! fixup! chore: code review comments + e2e tests

* fix: don't allow passing teamId for org membership create/update

* fix: improve docs and DTOs

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-07-18 03:43:20 +00:00
Morgan af3d2e6879 chore: set managed user locale via api v2 (#15794) 2024-07-17 12:56:37 +00:00
Morgan 58e61cb18e fix: findOrgUser use profiles instead of organizationId apiv2 (#15808) 2024-07-17 12:51:42 +00:00
NitinPSingh 9e3f8a2dc0 fix: Add signout button on onboarding screen (#15807)
* fix #15787 created signoutbtn on onboarding screen

* fix: Add signout button on onboarding screen and changes done as per feedback

* small error removed
2024-07-17 14:32:13 +02:00
Udit Takkar e960b8ad10 fix: fetch workflows on the backend (#15796)
* fix: fetch workflows on the router

* chore: remove workflows

* chore: import zod

* chore: move to repository
2024-07-17 07:02:22 +00:00
Peer Richelsen b7bfd77f95 readded border (#15798) 2024-07-16 15:46:40 +00:00
Peer Richelsen 2707adbb7b chore: removed cal.ai email assistant (#15793)
* removed cal-ai

* removed apps/ai
2024-07-16 14:21:00 +00:00
Morgan 5ef1b22c87 fix: remove permission blocking public apiv2 event-types endpoint (#15801) 2024-07-16 14:05:51 +00:00
Mohit YadavandKeith Williams 7e29db7a1b docs(contributing): change fork link (#15792)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-16 14:25:06 +02:00
41cde8e384 feat: API org user scope (#15739)
* Init organizations users GET endpoint

* Fix get users endpoint

* Add create user input

* Add POST to organization user endpoint

* Creating a user make email mandatory

* Move DTOs out of platform types

* Use plainToInstance for data filtering

* POST create user and org membership

* Add email check

* Filter update user call

* Add getOrg decorator to endpoints

* Add update endpoint

* Add delete endpoint

* Fix merge changes to create new org user

* Init tests

* Send org signup email

* Abstract username check

* Implement email service

* WIP E2E test

* Rename methods

* Update update org user DTO

* Remove unused inputs

* chore: add ApiProperty decorator to users class validators

* Type fix

* Update tests

* chore: code review comments

* chore: code review comments

* chore: code review comments

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-07-16 10:50:46 +00:00
MorganandLauris Skraucis 9fec77e345 fix: creating organization team should add owner (#15779)
* chore: platform libraries versions cleanup api v2

* fix: create membership to created org team apiv2

* chore: add test check owner membership

---------

Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
2024-07-16 10:31:29 +00:00
7046f6f80a feat: Fix Proton Calendar with ICS (#15454)
* feat: fix proton calendar with ics

* ics-feed: add note & clarify checkbox

* fix review

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-07-16 12:08:54 +02:00
Hariom Balhara 3f4a0eff7f Enable embed namespacing again (#15783) 2024-07-16 12:07:37 +02:00
Peer Richelsen d2768c4dc5 chore: Update /ee README.md (#15765)
* Update README.md

* Update README.md
2024-07-16 09:52:12 +00:00
470e0f53d8 feat: allow passing initial data props to calendar connectors (#15760)
* feat: allow passing init data to AvailabilitySettings, OAuthConnect wrappers

* chore: gitignore idea

* chore: minor fixes

* chore: use yarn.lock from main branch

---------

Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-07-16 09:47:50 +00:00
Lauris Skraucis 003af9a69d chore: specify platform-libraries version for event-types (#15785) 2024-07-16 09:47:37 +00:00
VijayandAmit Sharma 283ca9fc74 fix: #15749 Display availability page as per weekstart setting (#15753)
* update to display availability page as per weekstart setting

* update to accomodate any day

* update for AvailabilityTab

* reusable and tests added

* update test description

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-07-16 11:31:02 +02:00
Pranav RajveerandAmit Sharma 13ec930a61 fix: filter admin_api if org is present (#15386)
* fix: filter admin_api if org is present

* remove admin_api from developers tab when org is present

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-07-16 11:30:39 +02:00
Lauris Skraucis cada4a58ac chore: add platform-libraries change log (#15786) 2024-07-16 10:53:26 +02:00
Lauris Skraucis 77519a077c refactor: allow system admins to create event-type without membership (#15774) 2024-07-16 06:08:28 +00:00
Nick Holden 1a5a539106 fix: higher resolution Campsite icon (#15777) 2024-07-16 04:21:34 +00:00
Morgan 539a2ea301 chore: platform libraries versions cleanup api v2 (#15768)
* chore: platform libraries versions cleanup api v2

* fixup! chore: platform libraries versions cleanup api v2
2024-07-15 17:03:15 +00:00
Alex van Andel c8b577fb7a chore: Release v4.3.0 (#15771)
release.yml / release (push) Canceled after 0s
2024-07-15 13:55:09 +00:00
Peer Richelsen 7f97d27c20 chore: Update README.md (#15766)
* Update README.md

* Update README.md

* Update README.md
2024-07-15 13:40:28 +02:00
Morgan 9ff10877e7 chore: fix apiv2 org teams doc (#15746) 2024-07-15 10:27:33 +00:00
Morgan eb0768917c chore: readme platform libraries (#15737)
* chore: readme platform libraries

* fixup! chore: readme platform libraries

* fixup! fixup! chore: readme platform libraries

* Update README.md

* Update README.md
2024-07-15 10:13:41 +00:00
ec65cd6333 fix: icon visibility and add up and down icons (#15543)
* fixed the icons visibility and added up and down icon

* wip: TimeUnitAddonSuffix

* chore: Quick pass for rerender-optimisation

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-07-15 09:35:48 +00:00
Udit Takkar 563141b295 chore: add check for booking (#15740)
* chore: add check

* chore: missting no shoe
2024-07-15 08:59:29 +00:00
Hariom Balhara 7b24972900 fix: Ensure unique username in an organization (#15730) 2024-07-15 06:11:24 +00:00
Somay Chauhan 6d2ec71df3 add missing getServerSideProps to /event-types (#15731) 2024-07-13 09:51:00 +00:00
492304fe1d feat: BookerEmbed atom (#15689)
* init: booker embed atom

* update booker platform wrapper

* export booker embed for atoms

* update booker embed wrapper

* fixup: refresh url not needed

* rename booker embed

* add global styles

* export not needed

* add booker embed atoms in examples app

* fixup

* fixup! fixup

* fixup! fixup! fixup

---------

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-07-12 11:10:32 +00:00
c7c5cdf133 feat: Campsite conferencing app (#15713)
* feat: Campsite conferencing app

* Improve redirect after OAuth flow

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-07-11 14:20:31 +00:00
Keith Williams 0dd3b25624 chore: v4.2.5 (#15735)
release.yml / release (push) Canceled after 0s
2024-07-11 15:03:37 +02:00
Morgan a633e35c66 fix: api v2 unit tests (#15733)
* fix: permissions guard spec

* fix: api auth strategy spec
2024-07-11 11:27:40 +00:00
sean-brydon c618e69d8e fix: event type back button (#15722) 2024-07-11 10:44:37 +00:00
Morgan 844dd7e3af fix: remove console.log and tsignore apiv2 (#15732) 2024-07-11 10:40:11 +00:00
Morgan b6c512e5d7 fix: api-v2 controllers e2e tests (#15724)
* fix: bookings controllers e2e tests

* fixup! fix: bookings controllers e2e tests

* fix: calendars  controllers e2e tests

* chore: move org teams controller files into folder

* fix: gcal controller e2e tests

* fix: oauth-clients.controller.e2e-spec

* fix: calendars  controllers e2e tests

* fix: gcal controller e2e tests

* fix: oauth-flow.controller.e2e-spec.ts
2024-07-11 13:14:45 +03:00
Ankit SinghandKeith Williams 6e0035689a fix: Text in Embed Code Visibility Fixed (#15711)
* Fix:#15707

* bg-subtle fixed

* Update packages/features/embed/lib/EmbedTabs.tsx

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-10 16:18:31 +00:00
58540af9ce chore: sort calendar crendentials in event manager (#15448)
* chore_sort_calendar_credentials_in_desc_for_reliability

* chore: create reusable function

* refactor: rename functions

* Update EventManager.ts

Variable name change

---------

Co-authored-by: Shaik-Sirajuddin <sirajudddinshaik30@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2024-07-10 16:16:26 +00:00
Morgan 21f30fc47a chore: cache org guard and fix roles guard apiv2 (#15719) 2024-07-10 15:46:04 +00:00
Amit Sharma 92845d3adc Revert "fix: Autodetection of time zone only updated default time zone (#15392)" (#15720)
This reverts commit 285d1ec4c9.
2024-07-10 15:28:41 +00:00
8ee27d3997 feat: add POST end point to mark calls as no-shows (#15690)
* feat: add POST end point to mark calls as no-shows

* fix: type

* feat: add v2 endpoint

* wip

* chore: type err

* chore: add permission

* chore: remove packae

* chore: remove v1 API endpoint

* fix: endpoint

* feat: add new version

* chore: change route

* chore: error msg

* fix: message

* fix: type err

* chore: output

* fix: type err

* chore: bump libraries to 0.0.17

* chore: add noShowHost

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-07-10 13:01:10 +00:00
Morgan 1263519973 chore: add i18n to atoms (#15698)
* chore: add i18n to atoms

* fixup! chore: add i18n to atoms

* fixup! fixup! chore: add i18n to atoms

* fixup! Merge branch 'platform-atoms-i18n' of github.com:calcom/cal.com into platform-atoms-i18n

* fixup! Merge branch 'main' into platform-atoms-i18n
2024-07-10 12:34:41 +00:00
caf7943c91 fix: set conferencing apps as default (#15376)
* fix: set conferencing apps as default

* fix in other oauth conferencing apps

* creating default event-types should set the user's default location

* Update getDefaultLocations.ts

* fix: unit test

* default install only for first oauth app

* fix: type-check

* update default on success

* fix: pass callback prop

* chore: code cleanup

* Fix condition

* no longer needed

- as we reverted https://github.com/calcom/cal.com/commit/20fdb50081ae684723a5af78436068c0b09509a4

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-07-10 11:00:59 +00:00
Somay ChauhanandHariom Balhara fed3987028 fix: disable google-meet in user onboarding if google-calendar is not installed (#15702)
* fix: disable google-meet in user onboarding if google-calendar is not installed

* add tooltip

* fix: type check

* fix: type check for tooltip

- allowed `ReactNode` for tooltip content prop

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2024-07-10 10:34:14 +00:00
Hariom Balhara ad61d48706 fix: Rescheduling email when there is broken calendar integration (#15669) 2024-07-10 10:32:56 +01:00
Omar López aa20b9248a chore: v4.2.4 (#15703)
release.yml / release (push) Canceled after 0s
2024-07-09 14:09:04 +00:00
Keith Williams fa40f1ead5 fix: 404s becoming 500s (#15696) 2024-07-09 12:35:34 +02:00
Morgan 86676eb8af fix: delete reserved slot on booker unmount (#15700) 2024-07-09 10:08:06 +00:00
Morgan 3f9d1c1abf chore: add caching apiv2 roles guard (#15694) 2024-07-09 09:39:06 +00:00
5f8d090d3c refactor: handleNewBooking #3 (#15612)
* refactor: handleNewBooking #3

* fix: type error

* fix: type err

* fix: type err

* refactor: create booking factor

* chore: refactor availusers

* chore: type err

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-08 18:18:36 +00:00
4a2a2d165b fix: incorrect booking seats full error on collective seated event (#15602)
* fix: wrong booking seats full error on collective event

* test: verify seat availability calculation based on booked seats, not total attendees

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-07-08 18:02:02 +00:00
Martin SioneandJoe Au-Yeung 263c0ac7ef fix: Update stripe customer email when a user updates cal.com email (#15299)
* fix: Update stripe customer email when a user updates cal.com email

* Only update email on stripe if stripe is enabled and if there is a stripeAccountId already associated with the user

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-07-08 18:00:51 +00:00
f1b4d7d9e0 feat: app install flow followup (#15193)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2024-07-08 19:15:57 +02:00
Anik Dhabal BabuandKeith Williams 7d8cb7d6bb fix (#15465)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-08 14:17:56 +00:00
Hariom BalharaandKeith Williams 5ff7e65d64 fix: Broken navigation after booking creation in some cases (#15664)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-07-08 13:56:58 +00:00
5bbc025494 fix: 15636 Sort order of team event types incorrect (#15660)
* Fix:15636 event types sort order on team page

* fix type-check

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-07-08 14:31:27 +01:00
sean-brydon 59bffa075f remove (#15687) 2024-07-08 12:31:15 +00:00
c4104af8a2 feat: org-wide workflows (#15083)
* add org to create workflow button

* add save button for testing in dev mode

* add select all to multi select dropdown

* fix select all

* pass isOrg to WorkflowDetailsPage

* add checkbox to apply to all including future

* fix count text on select

* WIP schema

* shows teams in dropdown

* add team option to UI

* [WIP] refactor for update handler

* filter out "all" from activeOn

* fix type error

* create more helper functions

* create reusable function for scheduling all booking reminders

* finish update workflows for orgs (without testing)

* fix isAuthorized check for orgs

* fix getting newActiveOn

* move all helper functions to util file

* more code clean up

* fix deleting reminders for org workflows

* fix adding and editing steps

* update form data when workflow is saved

* code clean up

* fixing creating/deleting reminders when user is member of several teams

* fix setting activeOn for teams in useffect

* add on delete cascade

* make multiSelectSchenbox searchable

* set activeOn correctly when checkbox true

* WIP

* fix type errors in MultiSelectCheckboxes

* implemented scheduling org-wide workfow notifications (not yet tested)

* fix type errors

* type error

* add missing changes from merge conflict

* remote not needed include statement

* fix type errors

* code clean up + some todo comments

* support org workflows for cancelled workflows

* delete reminders from removed members

* remove reminders if isActiveOnAll is turned off

* fix unti test and type error

* code clean up

* create basis for testing to book org team event

* create org workflow with active team

* fix getting active org workflows on team + test setup

* fix creating workflow step for tests

* fix first org test

* add test for user event type with org workflow active

* use deleteAllWorkflowReminders everywhere

* add test for deleteRemindersFromRemovedActiveOn

* fix type errors

* make all tests pass

* fix type error

* fix getSchedule test

* code clean up

* add missing import

* fix type error

* fix tests

* code clean up

* fix imports

* update reminders when trigger was changed

* check for teamId before userId in reminderScheduler

* move getOrgIdFromMemberOrTeamId to different folder

* code clean up

* fix tests

* test setup for scheduleBookingReminders

* fix typo

* add tests for scheduleBookingReminders

* fix prisma default import

* fix workflowStep type

* add scheduleBookingReminders test for sms

* return dummy sid for scheduleSMS testMode

* clean up + fixes

* add lost changes from merge

* get teamId and userId from incoming evt object

* removing not needed select

* add org support for scheduleMandatory email reminder

* add other teams to dropdown

* move getAllWorkflows to seperate file and call it in parent function

* include org wide workflows in createNewSeat

* some fixes + code clean up

* add new team to select text count when including future teams is checked

* fix upsert and remove sms reminder field

* correctly update activeOn if 'including future ...' is enabled

* list active Org workflows in event workflows settings

* fix sms reminder field in all handlers

* add helper function to check if step was edited

* fix active on badge on workflow

* fix type error

* fix double reminders

* add teamId: null for userWorkflow query

* fix activeOnAll with managed event types

* add missing teamId in getAllWorkflows

* add a dafaut to prisma param

* fix managed event types on select all user workflows

* code clean up

* better variable name

* small fixes in update handler

* fix test name to match function name

* add info badge

* fix workflow count in event type settings

* fix getting bookings from children manged event types

* delete reminders when user is not part of any time no more

* implement feedback

* fix disbale workflow in event type settings

* fix remove member

* create new function getAllWorkflowsFromEventType

* add some removed code

* use promise.allSettled when deleting workflow reminders

* create new function deleteWorkflowRemindersOfRemovedMember.ts

* fix userId param

* delete org worklfows when team is disbanded

* don't trigger active on all workflow if not part of any team

* fix active on count badge

* add test for deleteWorkflowRemidnersOfRemovedMember

* trigger workflow also if not member of any subteam

* fix failing test

* remove unused code

* use testId for go back button

* fixes for managed event types & activateEventTypeHandler

* code clean up

* don't activate workflow on locked managed event type

* fix type error

* type error

* more type fixes

* feedback

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-07-08 17:44:11 +05:30
Rajiv SahalandRajiv Sahal dca2922ef8 fix: booker atom types (#15685)
Co-authored-by: Rajiv Sahal <rajivsahal@Rajivs-MacBook-Pro.local>
2024-07-08 12:00:55 +00:00
MOHD RAZAandAnik Dhabal Babu 8b5ef68652 fix: datePicker skeleton issue solved (#15675)
* datePicker skeleton issue solved

* Update

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-08 17:15:22 +05:30
RushikeshandAnik Dhabal Babu 5b2688dda0 fix: correct message and translation key (#15657)
* fix event_type_requres_google_cal  to requires_google_calendar

* revert the changes

* redirect to google calendar page

* add translation key for "requires_google_calendar_to_install"

* use key and add correct message

* use key

* fix

* revert

* correct message

* refactor event type steup tab UI

* small fix

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-05 14:35:08 +05:30
Udit TakkarandHariom 884ccc9b48 feat: RECORDING_TRANSCRIPTION_GENERATED webhook (#15589)
* feat: RECORDING_TRANSCRIPTION_READY webhook

* refactor: split into diff files and restructuring

* feat: finish batch processor finished

* chore: types

* fix: type error

* chore: change name of triggger

* test: add unit test

* fix: type

* fix: type err

* chore: test

* feat: add booking reference repository and other imp

* chore: log

* fix: import

* chore: type error

* fix: update test

* fix: test

* fix: test

* fix: add timeout

* chore: move beforeEach

* Mock missing env variables to test. These vars are not set in CI

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-07-05 07:39:23 +00:00
7d06d9f865 chore: remove personal branding if in an org and use org branding (#14872)
* chore: remove personal branding if in an org and use org branding

* fix: minor fix

* Update SettingsLayout.tsx

* Update SettingsLayout.tsx

* Update profile.tsx

* some fixes and e2e test

* fix: e2e test

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-07-05 00:35:46 +05:30
785ab8b74a feat: v1 API - add sort option on GET /bookings (#15374)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2024-07-04 06:11:47 +00:00
557498b80d feat: embed: remove "Preview" tab and instead split code/preview layout 50/50 vertically (#15492)
* feat: embed: remove "Preview" tab and instead split code/preview layout 50/50 vertically

* fix: use single preview tab

* fix e2e

* add border radius

* final e2e fix

* fix: don't show preview tab component

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-04 09:54:35 +05:30
Rajiv SahalandRajiv Sahal c3a532cff0 fix: platform seed script (#15617)
Co-authored-by: Rajiv Sahal <rajivsahal@Rajivs-MacBook-Pro.local>
2024-07-03 15:31:41 +00:00
ab5d4f1d03 chore: update formbricks-js to newest version (#15146)
* chore: update formbricks-js to newest version

* fix: Regenerate yarn.lock with right deps

* update formbricks-js version to 2.1

* Update yarn.lock

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-03 17:27:39 +02:00
VijayandAmit Sharma ae79154b83 fix: assignAllTeamMembers field wrongly set in db (#15654)
* fix for assignAllTeamMembers update

* update for true to false scenario

* Update packages/trpc/server/routers/viewer/eventTypes/update.handler.ts

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-07-03 17:23:14 +02:00
Anik Dhabal BabuandUdit Takkar e33c676ec5 fix: toast message is not visble and some refactor (#15649)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-07-03 18:28:48 +05:30
8cd9285cc6 fix: Remove/Disable Duplicate button in Read Only mode of an event-type (#15511)
* fix: Remove/Disable Duplicate button in Read Only mode of an event-type

* fix: use readonly variable

* fix: hiding for smaller devices

* update test cases

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-03 15:23:22 +05:30
Udit Takkar 4f76d4468e fix: passing booking id in the payload (#15642) 2024-07-03 07:25:14 +00:00
RushikeshandAnik Dhabal Babu 92bf1ee3c7 refactor: remove duplicate object key in common.json (#15626)
* remove duplicate object key in common.json

* Update common.json

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-02 08:42:01 +00:00
e9aec23fbe fix: typo in german translation (#15637)
* fix: #11213, #11215

* Use default colour

* Fix typo in german translation

* fix_the_conflict

---------

Co-authored-by: Siddharth Movaliya <siddraj1992@gmail.com>
Co-authored-by: Sean Brydon <sean@cal.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
2024-07-02 11:33:23 +05:30
a6a60969df fix: Redirect URL with query params (#13912)
* Fix for  issue #13899 Redirect URL

* Update bookingSuccessRedirect.ts

* Update bookingSuccessRedirect.ts

* fix: url params

* chore: remove unused

* chore: remove comment

* Update

---------

Co-authored-by: Vikas Verma <inboxvikasverma@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-01 22:18:47 +00:00
Atchyut Preetham PulavarthiandAmit91848 e9d828639a fix(@cal.com/web): update debounce logic to work correctly and not show non-premium usernames as premium (#15629)
* fix(@cal.com/web): update debounce logic to work correctly and not show non-premium usernames as premium

- removes redundant useMemo hook from UsernameTextfield.tsx & PremiumTextField.tsx
- cancles the debounceAPICall on every unmount of UsernameTextfield.tsx & PremiumTextfield.tsx via useEffect cleanup function

Fixes #15628

* fix(@cal.com/web): update the debounce logic to use the pre-existing useDebounce hook

- update the debounce logic in UsernameTextField.tsx & PremiumTextField.tsx to use the pre-existing useDebounce hook instead of loadsh debounce
- update the debounce delay to 600ms to be consistent with the signup.tsx behavior

Fixes #15628

* fix: signup debounce not working properly

---------

Co-authored-by: Amit91848 <samit91848@gmail.com>
2024-07-01 15:45:09 +00:00
Amit SharmaandAnik Dhabal Babu 726c938b6d fix: make toast consisten with brand colour (#15638)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-07-01 15:43:27 +00:00
Rushikesh b43aaaccc2 fix: use correct object keys (#15631)
* use connected_google_calendar translation key

* use requires_google_calendar translation key

* Remove unused ExistingGoogleCal component
2024-07-01 11:28:14 +00:00
Balthasar Huber 0b5f828307 fix: Add fromRedirectOfNonOrgLink in getServerSideProps of private links (#15306) 2024-06-29 09:17:25 +00:00
Anik Dhabal BabuandSomay Chauhan 3807e339ce fix: Provide event rescheduling option before the event end time (#15150)
* feat: Provide event rescheduling option

* update

* update

* add checker function

* refactor and update

* update

* fix type error

* Update bookings-single-view.tsx

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-06-28 19:09:33 +05:30
Hariom Balhara ececef96e5 feat: Support running webapp and embed-core playground on HTTPS. It allows to test SameSite cookie attribute easily in embed (#15272)
* feat: Support running webapp and embed-core playground on HTTPS. It allows to test SameSite attribute easily in embed

* Update yarn.lock

* feat: Support running webapp and embed-core playground on HTTPS. It allows to test SameSite attribute easily in embed

* Update yarn.lock

* Fix git-setup.sh

* Fix yarn.lock
2024-06-28 13:06:59 +01:00
sean-brydonandKeith Williams 50338dcd61 fix: Update pr.yml (#15611)
* Update pr.yml

* Added the types back so PR update triggers when labeled

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-06-28 10:02:50 +00:00
Amit Sharmaandsean-brydon 285d1ec4c9 fix: Autodetection of time zone only updated default time zone (#15392)
* fix: Autodetection of time zone only updated default time zone

* fix: description text

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-06-28 10:12:33 +01:00
Udit Takkar c458b297ae fix: backward compatibility for old cal rooms (#15609)
* fix: record icon for old rooms

* chore: change function name
2024-06-28 09:42:38 +01:00
Udit TakkarandKeith Williams a52f7efe88 refactor: move functions to separate files (#15590)
* refactor: move functions to separate files

* chore: remove inline function

* chore: type error

* fix: type error

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-06-27 22:05:11 +00:00
3e10e2db36 feat: adds env variable for email sender name (#15598)
* feat: adds env variable for email sender name

* update env declaration file

---------

Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-06-27 18:25:13 -03:00
Omar López dfbcf4742f v4.2.3 (#15604)
release.yml / release (push) Canceled after 0s
2024-06-27 22:52:12 +02:00
3a2a70d83a fix: getPublicEventType of NonOrgUser fetch OrgUser event-type of same username (#15587)
* fix: getPublicEventType of NonOrgUser fetch OrgUser event-type with same username

* Fix the test that was supposed to verify the scenario

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2024-06-27 20:32:19 +00:00
3948b8d203 feat: booking no show webhook (#15502)
* WIP

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

* WIP

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

* WIP

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

* Type fixes

* Update webhook.e2e.ts

* Update noShow.handler.ts

* Log failed results

* Updates tests

* Show generic error on frontend.

* test: add basic webhook service test

* fix: webhook end to end test

* fix: type error

* fix: test

---------

Signed-off-by: zomars <zomars@me.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-06-27 20:12:09 +00:00
Keith Williams 6207afdf03 fix: PR update E2E tests problems (#15603) 2024-06-27 19:12:59 +00:00
Lauris Skraucis 52c7a89785 feat: organization schedules endpoints (#15584)
* feat: organization schedules endpoints

* refactor: add IsUserInOrg guard

* refactor: separate schedules controller folder

* tests

* chore: add roles guard and test it

* refactor: dont use Promise.all

* refactor: rely on profiles instead of user.organizationId

* feat: add pagination to getOrganizationSchedules

* refactor: default skip=0 and take=250

* chore: regenerate swagger doc
2024-06-27 19:04:26 +00:00
Hariom BalharaandAlex van Andel dda4ce5338 fix-moving-a-team-with-just-owner (#15595)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-06-27 17:57:48 +00:00
a02747fc52 fix: update z-index for intercom app launcher (#15516)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-06-27 17:57:29 +00:00
Morgan 03311def07 fix: update version apiv2 platform libraries for createNewUsersConnectToOrgIfExists (#15601) 2024-06-27 17:14:00 +00:00
5e382ffd21 feat: apple calendar connect atom (#15510)
* init apple calendar service

* update apple calendar service logic

* update typing for CalendarApp interface

* add id and type for apple calendar

* fixup: logic for apple calendar service

* update logic for checkIfCalendarConnected function

* add apple calendar service to calendars module

* fixup

* update calendars to include cases for apple calendar

* fix imports

* init frontend for apple calendar connect

* add apple connect atom to connect atom

* fixup

* fixup

* add alias for latest version of platform library

* fix import paths

* custom hook for saving apple calendar credentials

* add apple calendar to examples app

* add interface for credentials syncing calendars

* bring calendars controller to orginal state

* refactor

* update custom hook to sync credentials

* update atom

* resolve merge conflicts

* add endpoint for syncing calendar credentials

* fixup

* update atom

* update useCheck to invalidate queries onSubmit

* fix typo

* fixup

* remove unused tokens respository

* rename controller

* cleanup

* fix: reset react queryClient on access token change

---------

Co-authored-by: Rajiv Sahal <rajivsahal@Rajivs-MacBook-Pro.local>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-06-27 15:51:27 +00:00
Udit Takkar 0e0dfcdd88 fix: reduce cal recording bitrate (#15588)
* fix: reduce cal recording bitrate

* chore: bitrate

* chore: add enable recording ui

* fix: genereate meeting token

* chore: add wait for recording
2024-06-27 15:43:20 +00:00
Morgan e260809b08 feat: organizations teams endpoints apiv2 (#15586)
* feat: organizations teams endpoints apiv2

* fixup! Merge branch 'main' into feat-api-v2-org-teams-endpoints

* chore: add roles guard to endpoints

* add tests

* fixup! Merge branch 'main' into feat-api-v2-org-teams-endpoints

* fixup! fixup! Merge branch 'main' into feat-api-v2-org-teams-endpoints

* fixup! fixup! fixup! Merge branch 'main' into feat-api-v2-org-teams-endpoints
2024-06-27 18:04:37 +02:00
sean-brydonandHariom Balhara 835443a0d1 fix: cancel sub when movingTeamToOrg (#15594)
* fix: cancel sub when movingTeamToOrg

* Cleanup code a little

* Update packages/trpc/server/routers/viewer/organizations/createTeams.handler.ts

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* Update packages/trpc/server/routers/viewer/organizations/createTeams.handler.ts

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2024-06-27 10:38:42 +00:00
Crowdin Bot 48842e72a6 New Crowdin translations by Github Action 2024-06-27 09:58:07 +00:00
Morgan ff8931f4a0 fix: path /me endpoint should capitalize timezone apiv2 (#15593) 2024-06-27 09:41:00 +00:00
Hariom Balhara 6670bbc1d7 fix: Error in team members migration during org onboarding (#15349)
* fix: Error in team members migration during org onboarding

* Add invitationMemberHandler tests

* Add unit tests

* Improve tests and refactor

* Improve tests and refactor

* Fix type issue

* Fix createNewUsersConnectToOrgIfExists args
2024-06-27 00:53:55 +00:00
Alex van Andel 750676f91a chore: Add dynamic import and trigger captureException (#15583) 2024-06-26 22:32:26 +00:00
Sangram Bahadur db60f8faed setting background color to red for remove button in profile section (#15585) 2024-06-26 20:52:20 +00:00
Somay Chauhan 2b3cb20fdf fix: office365video expiry_date (#15561)
* fix: office#65Video expiry_date

* Revert "fix: office#65Video expiry_date"

This reverts commit 6142287805b5c6a40e746126735745a5617334c7.

* Change logger.debug to logger.error for better visibility of all log cases
2024-06-26 18:12:36 +00:00
Morgan 7f1d45bb5d chore: minimum required roles guard api-v2 (#15576)
* chore: minimum required roles guard api-v2

* fixup! chore: minimum required roles guard api-v2

* fixup! fixup! chore: minimum required roles guard api-v2

* fixup! Merge branch 'chore-roles-guard-api-v2' of github.com:calcom/cal.com into chore-roles-guard-api-v2

* fixup! Merge branch 'chore-roles-guard-api-v2' of github.com:calcom/cal.com into chore-roles-guard-api-v2
2024-06-26 18:00:48 +00:00
Lauris Skraucis 6632c26063 fix: exclude web wrappers from dist (#15572) 2024-06-26 14:41:37 +00:00
Keith Williams 97a3189c12 chore: Update PR template (#15581) 2024-06-26 14:29:26 +00:00
Udit Takkar d25595b546 feat: add instant meeting expiry input (#15555)
* feat: add instant meeting expiry input

* fix: type err

* chore: remove type

* chore: update label

* chore: update column name
2024-06-26 13:56:05 +00:00
Keith Williams a28eb63d61 chore: Removed unwanted CI changes that made it into main (#15580) 2024-06-26 13:24:38 +00:00
Omar Lópezandsean-brydon 4f944ec12d hotfix: team billing on batch operations (#15570)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-06-26 12:06:07 +00:00
Keith Williams e33930e5c0 chore: Cache timezones by calcom version (#15556)
* chore: Cache timezones by calcom version

* Fix types
2024-06-26 11:43:02 +00:00
Lauris Skraucis 9613b9da6d chore: IsUserInOrd guard (#15579) 2024-06-26 11:21:39 +00:00
Morgan ec755b142c chore: IsTeamInOrg guard and decorator apiv2 (#15567) 2024-06-26 07:32:49 +00:00
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
920 changed files with 41341 additions and 9299 deletions
+4 -4
View File
@@ -15,15 +15,14 @@
# - You can not repackage or sell the codebase
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
#
# To enable enterprise-only features, as an admin, go to /auth/setup to select your license and follow
# instructions. This environment variable is deprecated although still supported for backward compatibility.
# @see https://console.cal.com
# To enable enterprise-only features please add your envrionment variable to the .env file then make your way to /auth/setup to select your license and follow instructions.
CALCOM_LICENSE_KEY=
# Signature token for the Cal.com License API (used for self-hosted integrations)
# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key
CAL_SIGNATURE_TOKEN=
# The route to the Cal.com License API
CALCOM_PRIVATE_API_ROUTE=
CALCOM_PRIVATE_API_ROUTE="https://goblin.cal.com"
# ***********************************************************************************************************
# - DATABASE ************************************************************************************************
@@ -185,6 +184,7 @@ API_KEY_PREFIX=cal_
# allow access to the nodemailer transports from the .env file. E-mail templates are accessible within lib/emails/
# Configures the global From: header whilst sending emails.
EMAIL_FROM='notifications@yourselfhostedcal.com'
EMAIL_FROM_NAME='Cal.com'
# Configure SMTP settings (@see https://nodemailer.com/smtp/).
# Configuration to receive emails locally (mailhog)
-39
View File
@@ -1,39 +0,0 @@
---
name: Bug report
about: Report any issues with the platform
title: ""
labels: bug
assignees: ""
---
Found a bug? Please fill out the sections below. 👍
### Issue Summary
A summary of the issue. This needs to be a clear detailed-rich summary.
### Steps to Reproduce
1. (for example) Went to ...
2. Clicked on...
3. ...
Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead?
### Actual Results
- What's happening right now that is different from what is expected
### Expected Results
- This is an ideal result that the system should get after the tests are performed
### Technical details
- Browser version, screen recording, console logs, network requests: You can make a recording with [Bird Eats Bug](https://birdeatsbug.com/).
- Node.js version
- Anything else that you think could be an issue.
### Evidence
- How was this tested? This is quite mandatory in terms of bugs. Providing evidence of your testing with screenshots or/and videos is an amazing way to prove the bug and a troubleshooting chance to find the solution.
+81
View File
@@ -0,0 +1,81 @@
name: Bug report
description: "Please make sure you have checked for any duplicate issues."
title: "(bug)"
labels: ["🐛 bug"]
assignees: []
body:
- type: textarea
id: issue-summary
attributes:
label: Issue Summary
description: A summary of the issue. This needs to be a clear detailed-rich summary.
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
value: |
1. (for example) Went to ...
2. Clicked on...
3. ...
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: other-information
attributes:
label: Other information
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
validations:
required: false
- type: checkboxes
id: environment
attributes:
label: Environment
options:
- label: app.cal.com
- label: Self-hosted Cal
- type: textarea
id: desktop-version
attributes:
label: Desktop (please complete the following information)
description: |
examples:
- **OS**: [e.g. iOS]
- **Browser**: [e.g. chrome, safari]
- **Version**: [e.g. 22]
value: |
- OS:
- Node:
- npm:
render: markdown
validations:
required: true
- type: markdown
id: nodejs-version
attributes:
value: |
#### Node.JS version
[e.g. v18.15.0]
- type: markdown
id: anything-else
attributes:
value: |
#### Anything else?
- Screen recording, console logs, network requests: You can make a recording with [Loom](https://www.loom.com).
- Anything else that you think could be an issue?
+2 -2
View File
@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions
url: https://go.cal.com/discord
about: Ask a general question about the project on our discord community
url: https://github.com/calcom/cal.com/discussions
about: Need help selfhosting or ask a general question about the project? Open a discussion
+2 -1
View File
@@ -2,7 +2,7 @@
name: Feature request
about: Suggest a feature or idea
title: ""
labels: feature
labels: ["✨ feature", "🚨 needs approval"]
assignees: ""
---
@@ -53,3 +53,4 @@ assignees: ""
- If this issue has a `🚨 needs approval` label, don't start coding yet. Wait until a core member approves feature request by removing this label, then you can start coding.
- For clarity: Non-core member issues automatically get the `🚨 needs approval` label.
- Your feature ideas are invaluable to us! However, they undergo review to ensure alignment with the product's direction.
- Follow Best Practices lined out in our [Contributor Docs](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
+3 -3
View File
@@ -11,9 +11,9 @@
## Mandatory Tasks (DO NOT REMOVE)
- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected)
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com)
- [ ] I have added or modified automated tests that prove my fix is effective or that my feature works (PRs might be rejected if logical changes are not properly tested)
- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com). If N/A, write N/A here and check the checkbox.
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works.
## How should this be tested?
+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
+2 -1
View File
@@ -17,6 +17,7 @@ runs:
cache-name: cache-db
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
key-2: ${{ github.event.pull_request.number || github.ref }}
key-3: ${{ github.event.pull_request.head.sha }}
DATABASE_URL: ${{ inputs.DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ inputs.DATABASE_URL }}
E2E_TEST_CALCOM_QA_EMAIL: ${{ inputs.E2E_TEST_CALCOM_QA_EMAIL }}
@@ -24,7 +25,7 @@ runs:
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ inputs.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}
- run: echo ${{ env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} && yarn db-seed
if: steps.cache-db.outputs.cache-hit != 'true'
shell: bash
-6
View File
@@ -69,12 +69,6 @@ jobs:
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: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
if: always()
@@ -3,6 +3,9 @@ name: Production Builds
on:
workflow_call:
permissions:
contents: read
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
@@ -65,7 +68,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
@@ -10,6 +10,8 @@ env:
jobs:
build:
name: Build API v2
permissions:
contents: read
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 30
services:
@@ -34,7 +36,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
+2
View File
@@ -3,6 +3,8 @@ on:
workflow_call:
env:
NODE_OPTIONS: --max-old-space-size=4096
permissions:
contents: read
jobs:
check-types:
runs-on: buildjet-4vcpu-ubuntu-2204
+79
View File
@@ -0,0 +1,79 @@
name: E2E
on:
workflow_call:
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
API_KEY_PREFIX: ${{ secrets.CI_API_KEY_PREFIX }}
API_PORT: ${{ vars.CI_API_V2_PORT }}
CALCOM_LICENSE_KEY: ${{ secrets.CI_CALCOM_LICENSE_KEY }}
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_READ_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_WRITE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
IS_E2E: true
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NODE_OPTIONS: --max-old-space-size=4096
REDIS_URL: "redis://localhost:6379"
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
jobs:
e2e:
timeout-minutes: 20
name: E2E API v2
runs-on: buildjet-8vcpu-ubuntu-2204
services:
postgres:
image: postgres:13
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: calendso
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:latest
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
fail-fast: false
steps:
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- uses: ./.github/actions/cache-db
- name: Run Tests
working-directory: apps/api/v2
run: |
yarn test:e2e
EXIT_CODE=$?
echo "yarn test:e2e command exit code: $EXIT_CODE"
exit $EXIT_CODE
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: test-results-api-v2
path: test-results
+3
View File
@@ -1,6 +1,9 @@
name: E2E App Store Tests
on:
workflow_call:
permissions:
actions: write
contents: read
env:
NODE_OPTIONS: --max-old-space-size=4096
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
+3
View File
@@ -1,6 +1,9 @@
name: E2E Embed React tests and booking flow (for non-embed as well)
on:
workflow_call:
permissions:
actions: write
contents: read
env:
NODE_OPTIONS: --max-old-space-size=4096
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
+3
View File
@@ -1,6 +1,9 @@
name: E2E Embed Core tests and booking flow (for non-embed as well)
on:
workflow_call:
permissions:
actions: write
contents: read
env:
NODE_OPTIONS: --max-old-space-size=4096
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
+3
View File
@@ -1,6 +1,9 @@
name: E2E
on:
workflow_call:
permissions:
actions: write
contents: read
env:
NODE_OPTIONS: --max-old-space-size=4096
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
+2
View File
@@ -1,6 +1,8 @@
name: Integration
on:
workflow_call:
permissions:
contents: read
env:
NODE_OPTIONS: --max-old-space-size=4096
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
+3 -7
View File
@@ -1,6 +1,9 @@
name: Lint
on:
workflow_call:
permissions:
actions: write
contents: read
jobs:
lint:
runs-on: buildjet-4vcpu-ubuntu-2204
@@ -16,13 +19,6 @@ jobs:
- name: Merge lint reports
run: jq -s '[.[]]|flatten' lint-results/*.json &> lint-results/eslint_report.json
- name: Annotate Code Linting Results
uses: ataylorme/eslint-annotate-action@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
report-json: "lint-results/eslint_report.json"
only-pr-files: false
- name: Upload ESLint report
if: ${{ always() }}
uses: actions/upload-artifact@v4
@@ -0,0 +1,81 @@
name: "Next.js Bundle Analysis Annotation"
on:
workflow_call:
workflow_dispatch:
permissions:
actions: read
contents: read
pull-requests: write
jobs:
annotate:
if: always()
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/cache-build
- name: Download base branch bundle stats
uses: dawidd6/action-download-artifact@v2
if: success()
with:
workflow: nextjs-bundle-analysis.yml
branch: ${{ github.event.pull_request.base.ref }}
path: apps/web/.next/analyze/base
# And here's the second place - this runs after we have both the current and
# base branch bundle stats, and will compare them to determine what changed.
# There are two configurable arguments that come from package.json:
#
# - budget: optional, set a budget (bytes) against which size changes are measured
# it's set to 350kb here by default, as informed by the following piece:
# https://infrequently.org/2021/03/the-performance-inequality-gap/
#
# - red-status-percentage: sets the percent size increase where you get a red
# status indicator, defaults to 20%
#
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
# entry in your package.json file.
- name: Compare with base branch bundle
if: success()
run: |
cd apps/web
ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
- name: Get comment body
id: get-comment-body
if: success() && github.event.number
run: |
cd apps/web
body=$(cat .next/analyze/__bundle_analysis_comment.txt)
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find Comment
uses: peter-evans/find-comment@v2
if: success() && github.event.number
id: fc
with:
issue-number: ${{ github.event.number }}
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
- name: Create Comment
uses: peter-evans/create-or-update-comment@v3
if: success() && github.event.number && steps.fc.outputs.comment-id == 0
with:
issue-number: ${{ github.event.number }}
body: ${{ steps.get-comment-body.outputs.body }}
- name: Update Comment
uses: peter-evans/create-or-update-comment@v3
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
with:
issue-number: ${{ github.event.number }}
body: ${{ steps.get-comment-body.outputs.body }}
comment-id: ${{ steps.fc.outputs.comment-id }}
edit-mode: replace
+4 -68
View File
@@ -7,6 +7,10 @@ on:
branches:
- main
permissions:
actions: write
contents: read
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
@@ -45,13 +49,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:
@@ -69,65 +67,3 @@ jobs:
with:
name: bundle
path: apps/web/.next/analyze/__bundle_analysis.json
- name: Download base branch bundle stats
uses: dawidd6/action-download-artifact@v2
if: success()
with:
workflow: nextjs-bundle-analysis.yml
branch: ${{ github.event.pull_request.base.ref }}
path: apps/web/.next/analyze/base
# And here's the second place - this runs after we have both the current and
# base branch bundle stats, and will compare them to determine what changed.
# There are two configurable arguments that come from package.json:
#
# - budget: optional, set a budget (bytes) against which size changes are measured
# it's set to 350kb here by default, as informed by the following piece:
# https://infrequently.org/2021/03/the-performance-inequality-gap/
#
# - red-status-percentage: sets the percent size increase where you get a red
# status indicator, defaults to 20%
#
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
# entry in your package.json file.
- name: Compare with base branch bundle
if: success()
run: |
cd apps/web
ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
- name: Get comment body
id: get-comment-body
if: success() && github.event.number
run: |
cd apps/web
body=$(cat .next/analyze/__bundle_analysis_comment.txt)
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find Comment
uses: peter-evans/find-comment@v2
if: success() && github.event.number
id: fc
with:
issue-number: ${{ github.event.number }}
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
- name: Create Comment
uses: peter-evans/create-or-update-comment@v3
if: success() && github.event.number && steps.fc.outputs.comment-id == 0
with:
issue-number: ${{ github.event.number }}
body: ${{ steps.get-comment-body.outputs.body }}
- name: Update Comment
uses: peter-evans/create-or-update-comment@v3
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
with:
issue-number: ${{ github.event.number }}
body: ${{ steps.get-comment-body.outputs.body }}
comment-id: ${{ steps.fc.outputs.comment-id }}
edit-mode: replace
+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'
+131 -5
View File
@@ -2,10 +2,16 @@ name: PR Update
on:
pull_request_target:
types: [opened, synchronize, reopened, labeled]
branches:
- main
- gh-actions-test-branch
workflow_dispatch:
permissions:
actions: write
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
@@ -18,6 +24,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,36 +34,155 @@ 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
permissions:
pull-requests: read
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
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
integration-test:
name: Tests
needs: [changes]
needs: [changes, check-label, build, build-api-v1, build-api-v2]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/integration-tests.yml
secrets: inherit
e2e:
name: Tests
needs: [changes, check-label, build, build-api-v1, build-api-v2]
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-api-v2:
name: Tests
needs: [changes, check-label, build, build-api-v1, build-api-v2]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-api-v2.yml
secrets: inherit
e2e-app-store:
name: Tests
needs: [changes, check-label, build, build-api-v1, build-api-v2]
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: Tests
needs: [changes, check-label, build, build-api-v1, build-api-v2]
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: Tests
needs: [changes, check-label, build, build-api-v1, build-api-v2]
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]
needs: [changes, lint, type-check, unit-test, integration-test, check-label, build, build-api-v1, build-api-v2, e2e, e2e-api-v2, e2e-embed, e2e-embed-react, e2e-app-store]
if: always()
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
-77
View File
@@ -1,77 +0,0 @@
name: Pre-release checks
on:
workflow_dispatch:
jobs:
changes:
name: Detect changes
runs-on: buildjet-4vcpu-ubuntu-2204
permissions:
pull-requests: read
outputs:
app-store: ${{ steps.filter.outputs.app-store }}
embed: ${{ steps.filter.outputs.embed }}
embed-react: ${{ steps.filter.outputs.embed-react }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
app-store:
- 'apps/web/**'
- 'packages/app-store/**'
- 'playwright.config.ts'
embed:
- 'apps/web/**'
- 'packages/embeds/**'
- 'playwright.config.ts'
embed-react:
- 'apps/web/**'
- 'packages/embeds/**'
- 'playwright.config.ts'
lint:
name: Linters
uses: ./.github/workflows/lint.yml
secrets: inherit
build:
name: Production builds
uses: ./.github/workflows/production-build-without-database.yml
secrets: inherit
e2e:
name: E2E tests
needs: [changes, lint, build]
uses: ./.github/workflows/e2e.yml
secrets: inherit
e2e-app-store:
name: E2E App Store tests
needs: [changes, lint, build]
uses: ./.github/workflows/e2e-app-store.yml
secrets: inherit
e2e-embed:
name: E2E embeds tests
needs: [changes, lint, build]
uses: ./.github/workflows/e2e-embed.yml
secrets: inherit
e2e-embed-react:
name: E2E React embeds tests
needs: [changes, lint, build]
uses: ./.github/workflows/e2e-embed-react.yml
secrets: inherit
required:
needs: [changes, lint, build, e2e, e2e-app-store, e2e-embed, e2e-embed-react]
if: always()
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
- name: fail if conditional jobs failed
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
run: exit 1
@@ -2,6 +2,9 @@ name: Production Builds
on:
workflow_call:
permissions:
contents: read
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
+7 -4
View File
@@ -1,9 +1,8 @@
name: Unit
on:
workflow_call:
workflow_run:
workflows: [Crowdin Action]
types: [completed]
permissions:
contents: read
jobs:
test:
name: Unit
@@ -13,7 +12,11 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
- run: yarn test
# We could add different timezones here that we need to run our tests in
- run: TZ=America/Los_Angeles yarn test -- --timeZoneDependentTestsOnly
- name: Run API v2 tests
working-directory: apps/api/v2
run: |
export NODE_OPTIONS="--max_old_space_size=8192"
yarn test
+3
View File
@@ -3,6 +3,9 @@ name: Yarn install
on:
workflow_call:
permissions:
contents: read
jobs:
setup:
name: Yarn install & cache
+1 -1
View File
@@ -74,7 +74,7 @@ branch are tagged into a release monthly.
To develop locally:
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
1. [Fork](https://github.com/calcom/cal.com/fork/) this repository to your
own GitHub account and then
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
2. Create a new branch:
+31 -1
View File
@@ -392,7 +392,37 @@ Currently Vercel Pro Plan is required to be able to Deploy this application with
See the [roadmap project](https://cal.com/roadmap) for a list of proposed features (and known issues). You can change the view to see planned tagged releases.
<!-- RORADMAP -->
<!-- LICENSE -->
## License
Cal.com, Inc. is a commercial open source company, which means some parts of this open source repository require a commercial license. The concept is called "Open Core" where the core technology (99%) is fully open source, licensed under [AGPLv3](https://opensource.org/license/agpl-v3) and the last 1% is covered under a commercial license (["/ee" Enterprise Edition](https://github.com/calcom/cal.com/tree/main/packages/features/ee)) which we believe is entirely relevant for larger organisations that require enterprise features. Enterprise features are built by the core engineering team of Cal.com, Inc. which is hired in full-time. Find their compensation on https://cal.com/open.
> [!NOTE]
> Our philosophy is simple, all "Singleplayer APIs" are open-source under AGPLv3. All commercial "Multiplayer APIs" are under a commercial license.
| | AGPLv3 | EE |
|---|---|---|
| Self-host for commercial purposes | ✅ | ✅ |
| Clone privately | ✅ | ✅ |
| Fork publicly | ✅ | ✅ |
| Requires CLA | ✅ | ✅ |
| Official Support| ❌ | ✅ |
| Derivative work privately | ❌ | ✅ |
| SSO | ❌ | ✅ |
| Admin Panel | ❌ | ✅ |
| Impersonation | ❌ | ✅ |
| Managed Event Types | ❌ | ✅ |
| Organizations | ❌ | ✅ |
| Payments | ❌ | ✅ |
| Platform | ❌ | ✅ |
| Teams | ❌ | ✅ |
| Users | ❌ | ✅ |
| Video | ❌ | ✅ |
| Workflows | ❌ | ✅ |
> [!TIP]
> We work closely with the community and always invite feedback about what should be open and what is fine to be commercial. This list is not set and stone and we have moved things from commercial to open in the past. Please open a [discussion](https://github.com/calcom/cal.com/discussions) if you feel like something is wrong.
## Repo Activity
-2
View File
@@ -4,8 +4,6 @@ export function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// reduce sample rate to 10% on production
tracesSampleRate: process.env.NODE_ENV !== "production" ? 1.0 : 0.1,
});
}
@@ -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();
};
+6 -3
View File
@@ -1,7 +1,7 @@
import type { NextMiddleware } from "next-api-middleware";
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
@@ -18,9 +18,12 @@ export const dateNotInPast = function (date: Date) {
// This verifies the apiKey and sets the user if it is valid.
export const verifyApiKey: NextMiddleware = async (req, res, next) => {
const hasValidLicense = await checkLicense(prisma);
if (!hasValidLicense && IS_PRODUCTION)
const licenseKeyService = await LicenseKeyService.create();
const hasValidLicense = await licenseKeyService.checkLicense();
if (!hasValidLicense && IS_PRODUCTION) {
return res.status(401).json({ error: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
}
// Check if the apiKey query param is provided.
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
// remove the prefix from the user provided api_key. If no env set default to "cal_"
+2
View File
@@ -21,6 +21,8 @@ export const schemaBookingCreateBodyParams = extendedBookingCreateBody.merge(sch
export const schemaBookingGetParams = z.object({
dateFrom: iso8601.optional(),
dateTo: iso8601.optional(),
order: z.enum(["asc", "desc"]).default("asc"),
sortBy: z.enum(["createdAt", "updatedAt"]).optional(),
});
const schemaBookingEditParams = z
+25 -1
View File
@@ -51,6 +51,18 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
* type: string
* format: email
* example: [john.doe@example.com, jane.doe@example.com]
* - in: query
* name: order
* required: false
* schema:
* type: string
* enum: [asc, desc]
* - in: query
* name: sortBy
* required: false
* schema:
* type: string
* enum: [createdAt, updatedAt]
* operationId: listBookings
* tags:
* - bookings
@@ -177,7 +189,7 @@ export async function handler(req: NextApiRequest) {
isOrganizationOwnerOrAdmin,
pagination: { take, skip },
} = req;
const { dateFrom, dateTo } = schemaBookingGetParams.parse(req.query);
const { dateFrom, dateTo, order, sortBy } = schemaBookingGetParams.parse(req.query);
const args: Prisma.BookingFindManyArgs = {};
if (req.query.take && req.query.page) {
@@ -251,6 +263,18 @@ export async function handler(req: NextApiRequest) {
};
}
if (sortBy === "updatedAt") {
args.orderBy = {
updatedAt: order,
};
}
if (sortBy === "createdAt") {
args.orderBy = {
createdAt: order,
};
}
const data = await prisma.booking.findMany(args);
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
}
+4
View File
@@ -47,6 +47,10 @@ import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
* type: string
* format: date-time
* description: 'End time of the Event'
* rescheduleUid:
* type: string
* format: UID
* description: 'Uid of the booking to reschedule'
* responses:
* type: object
* required:
-1
View File
@@ -43,7 +43,6 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
language: data.language,
teamId: data.teamId,
usernameOrEmail: data.usernameOrEmail,
isOrg: data.isOrg,
});
return { success: true, message: `${data.usernameOrEmail} has been invited.` };
+13 -4
View File
@@ -69,6 +69,9 @@ import { schemaTeamCreateBodyParams, schemaTeamReadPublic } from "~/lib/validati
* ownerId:
* type: number
* description: ID of the team owner - only admins can set this.
* parentId:
* type: number
* description: ID of the parent organization.
* tags:
* - teams
* responses:
@@ -99,7 +102,7 @@ async function postHandler(req: NextApiRequest) {
if (alreadyExist) throw new HttpError({ statusCode: 409, message: "Team slug already exists" });
}
// Check if parentId is related to this user
// Check if parentId is related to this user and is an organization
if (data.parentId) {
const parentTeam = await prisma.team.findFirst({
where: { id: data.parentId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
@@ -107,7 +110,13 @@ async function postHandler(req: NextApiRequest) {
if (!parentTeam)
throw new HttpError({
statusCode: 401,
message: "Unauthorized: Invalid parent id. You can only use parent id of your own teams.",
message: "Unauthorized: Invalid parent id. You can only use parent id if you are org owner or admin.",
});
if (parentTeam.parentId)
throw new HttpError({
statusCode: 400,
message: "parentId must be of an organization, not a team.",
});
}
@@ -119,7 +128,7 @@ async function postHandler(req: NextApiRequest) {
metadata: data.metadata === null ? {} : data.metadata || undefined,
};
if (!IS_TEAM_BILLING_ENABLED) {
if (!IS_TEAM_BILLING_ENABLED || data.parentId) {
const team = await prisma.team.create({
data: {
...cloneData,
@@ -135,7 +144,7 @@ async function postHandler(req: NextApiRequest) {
return {
team: schemaTeamReadPublic.parse(team),
owner: schemaMembershipPublic.parse(team.members[0]),
message: `Team created successfully. We also made user with ID=${ownerId} the owner of this team.`,
message: `Team created successfully. We also made user with ID=${effectiveUserId} the owner of this team.`,
};
}
@@ -0,0 +1,112 @@
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 } from "vitest";
import handler from "../../../pages/api/attendees/_post";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
describe("POST /api/attendees", () => {
describe("Errors", () => {
test("Returns 403 if user is not admin and has no booking", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
bookingId: 1,
email: "test@example.com",
name: "Test User",
timeZone: "UTC",
},
});
prismaMock.booking.findFirst.mockResolvedValue(null);
req.userId = 123;
// req.isAdmin = false;
await handler(req, res);
expect(res.statusCode).toBe(403);
expect(JSON.parse(res._getData()).message).toBe("Forbidden");
});
test("Returns 200 if user is admin", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
bookingId: 1,
email: "test@example.com",
name: "Test User",
timeZone: "UTC",
},
});
const attendeeData = {
id: 1,
email: "test@example.com",
name: "Test User",
timeZone: "UTC",
bookingId: 1,
};
prismaMock.attendee.create.mockResolvedValue(attendeeData);
req.isSystemWideAdmin = true;
req.userId = 123;
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(JSON.parse(res._getData()).attendee).toEqual(attendeeData);
expect(JSON.parse(res._getData()).message).toBe("Attendee created successfully");
});
test("Returns 200 if user is not admin but has a booking", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
bookingId: 1,
email: "test@example.com",
name: "Test User",
timeZone: "UTC",
},
});
const userBooking = { id: 1 };
prismaMock.booking.findFirst.mockResolvedValue(userBooking);
const attendeeData = {
id: 1,
email: "test@example.com",
name: "Test User",
timeZone: "UTC",
bookingId: 1,
};
prismaMock.attendee.create.mockResolvedValue(attendeeData);
req.userId = 123;
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(JSON.parse(res._getData()).attendee).toEqual(attendeeData);
expect(JSON.parse(res._getData()).message).toBe("Attendee created successfully");
});
test("Returns 400 if request body is invalid", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
// Missing required fields
},
});
req.userId = 123;
await handler(req, res);
expect(res.statusCode).toBe(400);
});
});
});
+37 -1
View File
@@ -11,6 +11,7 @@ import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
import prisma from "@calcom/prisma";
import type { Booking } from "@calcom/prisma";
import handler from "../../../pages/api/bookings/_post";
@@ -157,6 +158,7 @@ describe.skipIf(true)("POST /api/bookings", () => {
describe("Success", () => {
describe("Regular event-type", () => {
let createdBooking: Booking;
test("Creates one single booking", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
@@ -181,9 +183,43 @@ describe.skipIf(true)("POST /api/bookings", () => {
await handler(req, res);
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
createdBooking = JSON.parse(res._getData());
expect(prismaMock.booking.create).toHaveBeenCalledTimes(1);
});
test("Reschedule created booking", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
name: "testReschedule",
start: dayjs().add(2, "day").format(),
end: dayjs().add(3, "day").format(),
eventTypeId: 2,
email: "test@example.com",
location: "Cal.com Video",
timeZone: "America/Montevideo",
language: "en",
customInputs: [],
metadata: {},
userId: 4,
rescheduleUid: createdBooking.uid,
},
prisma,
});
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue(buildEventType());
prismaMock.booking.findMany.mockResolvedValue([]);
await handler(req, res);
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
const rescheduledBooking = JSON.parse(res._getData()) as Booking;
expect(prismaMock.booking.create).toHaveBeenCalledTimes(1);
expect(rescheduledBooking.fromReschedule).toEqual(createdBooking.uid);
const previousBooking = await prisma.booking.findUnique({
where: { uid: createdBooking.uid },
});
expect(previousBooking?.status).toBe("cancelled");
});
});
describe("Recurring event-type", () => {
@@ -3,9 +3,9 @@ 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, vi, it, expect, afterEach } from "vitest";
import { describe, vi, it, expect, afterEach, beforeEach } from "vitest";
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
import prisma from "@calcom/prisma";
import { isAdminGuard } from "~/lib/utils/isAdmin";
@@ -20,20 +20,21 @@ afterEach(() => {
vi.resetAllMocks();
});
vi.mock("@calcom/features/ee/common/server/checkLicense", () => {
return {
default: vi.fn(),
};
});
vi.mock("~/lib/utils/isAdmin", () => {
return {
isAdminGuard: vi.fn(),
};
});
vi.mock("@calcom/prisma");
vi.mock("~/lib/utils/isAdmin", () => ({
isAdminGuard: vi.fn(),
}));
describe("Verify API key", () => {
it("It should throw an error if the api key is not valid", async () => {
let service: LicenseKeyService;
beforeEach(async () => {
service = await LicenseKeyService.create();
vi.spyOn(service, "checkLicense");
});
it("should throw an error if the api key is not valid", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {},
@@ -43,7 +44,7 @@ describe("Verify API key", () => {
fn: verifyApiKey,
};
vi.mocked(checkLicense).mockResolvedValue(false);
vi.mocked(service.checkLicense).mockResolvedValue(false);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null });
const serverNext = vi.fn((next: void) => Promise.resolve(next));
@@ -53,9 +54,11 @@ describe("Verify API key", () => {
await middleware.fn(req, res, serverNext);
expect(middlewareSpy).toBeCalled();
expect(res.statusCode).toBe(401);
});
it("It should throw an error if no api key is provided", async () => {
it("should throw an error if no api key is provided", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {},
@@ -65,7 +68,7 @@ describe("Verify API key", () => {
fn: verifyApiKey,
};
vi.mocked(checkLicense).mockResolvedValue(true);
vi.mocked(service.checkLicense).mockResolvedValue(true);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null });
const serverNext = vi.fn((next: void) => Promise.resolve(next));
@@ -75,10 +78,11 @@ describe("Verify API key", () => {
await middleware.fn(req, res, serverNext);
expect(middlewareSpy).toBeCalled();
expect(res.statusCode).toBe(401);
});
it("It should set correct permissions for system-wide admin", async () => {
it("should set correct permissions for system-wide admin", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {},
@@ -97,7 +101,7 @@ describe("Verify API key", () => {
fn: verifyApiKey,
};
vi.mocked(checkLicense).mockResolvedValue(true);
vi.mocked(service.checkLicense).mockResolvedValue(true);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.SystemWide });
const serverNext = vi.fn((next: void) => Promise.resolve(next));
@@ -107,11 +111,12 @@ describe("Verify API key", () => {
await middleware.fn(req, res, serverNext);
expect(middlewareSpy).toBeCalled();
expect(req.isSystemWideAdmin).toBe(true);
expect(req.isOrganizationOwnerOrAdmin).toBe(false);
});
it("It should set correct permissions for org-level admin", async () => {
it("should set correct permissions for org-level admin", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {},
@@ -130,7 +135,7 @@ describe("Verify API key", () => {
fn: verifyApiKey,
};
vi.mocked(checkLicense).mockResolvedValue(true);
vi.mocked(service.checkLicense).mockResolvedValue(true);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin });
const serverNext = vi.fn((next: void) => Promise.resolve(next));
@@ -140,6 +145,7 @@ describe("Verify API key", () => {
await middleware.fn(req, res, serverNext);
expect(middlewareSpy).toBeCalled();
expect(req.isSystemWideAdmin).toBe(false);
expect(req.isOrganizationOwnerOrAdmin).toBe(true);
});
@@ -0,0 +1,121 @@
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 } from "vitest";
import { HttpError } from "@calcom/lib/http-error";
import handler from "../../../pages/api/selected-calendars/_post";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
describe("POST /api/selected-calendars", () => {
describe("Errors", () => {
test("Returns 403 if non-admin user tries to set userId in body", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
integration: "google",
externalId: "ext123",
userId: 444444,
},
});
req.userId = 333333;
try {
await handler(req, res);
} catch (e) {
expect(e).toBeInstanceOf(HttpError);
expect((e as HttpError).statusCode).toBe(403);
expect((e as HttpError).message).toBe("ADMIN required for userId");
}
});
test("Returns 400 if request body is invalid", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
integration: "google",
},
});
req.userId = 333333;
req.isSystemWideAdmin = true;
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(JSON.parse(res._getData()).message).toBe("invalid_type in 'externalId': Required");
});
});
describe("Success", () => {
test("Creates selected calendar if user is admin and sets bodyUserId", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
query: {
apiKey: "validApiKey",
},
body: {
integration: "google",
externalId: "ext123",
userId: 444444,
},
});
req.userId = 333333;
req.isSystemWideAdmin = true;
prismaMock.user.findFirstOrThrow.mockResolvedValue({
id: 444444,
} as any);
prismaMock.selectedCalendar.create.mockResolvedValue({
credentialId: 1,
integration: "google",
externalId: "ext123",
userId: 444444,
});
await handler(req, res);
expect(res.statusCode).toBe(200);
const responseData = JSON.parse(res._getData());
expect(responseData.selected_calendar.credentialId).toBe(1);
expect(responseData.message).toBe("Selected Calendar created successfully");
});
test("Creates selected calendar if user is non-admin and does not set bodyUserId", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
query: {
apiKey: "validApiKey",
},
body: {
integration: "google",
externalId: "ext123",
},
});
req.userId = 333333;
prismaMock.selectedCalendar.create.mockResolvedValue({
credentialId: 1,
integration: "google",
externalId: "ext123",
userId: 333333,
});
await handler(req, res);
expect(res.statusCode).toBe(200);
const responseData = JSON.parse(res._getData());
expect(responseData.selected_calendar.credentialId).toBe(1);
expect(responseData.message).toBe("Selected Calendar created successfully");
});
});
});
+7
View File
@@ -14,9 +14,16 @@ NEXT_PUBLIC_SENTRY_DSN=
# Stripe Billing
STRIPE_PRICE_ID_STARTER=
STRIPE_PRICE_ID_STARTER_OVERAGE=
STRIPE_PRICE_ID_ESSENTIALS=
STRIPE_PRICE_ID_ESSENTIALS_OVERAGE=
STRIPE_PRICE_ID_ENTERPRISE=
STRIPE_PRICE_ID_ENTERPRISE_OVERAGE=
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
+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
+2 -1
View File
@@ -10,5 +10,6 @@
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"setupFiles": ["<rootDir>/test/setEnvVars.ts"]
"setupFiles": ["<rootDir>/test/setEnvVars.ts"],
"reporters": ["default", "jest-summarizing-reporter"]
}
+4 -2
View File
@@ -18,7 +18,7 @@
"test:watch": "yarn dev:build && jest --watch",
"test:cov": "yarn dev:build && jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "yarn dev:build && jest --runInBand --config ./jest-e2e.json",
"test:e2e": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json",
"prisma": "yarn workspace @calcom/prisma prisma",
"generate-schemas": "yarn prisma generate && yarn prisma format",
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts"
@@ -26,12 +26,13 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@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-libraries-0.0.22": "npm:@calcom/platform-libraries@0.0.22",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
"@golevelup/ts-jest": "^0.4.0",
"@microsoft/microsoft-graph-types-beta": "^0.42.0-preview",
"@nestjs/bull": "^10.1.1",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
@@ -42,6 +43,7 @@
"@nestjs/throttler": "^5.1.2",
"@sentry/node": "^8.8.0",
"body-parser": "^1.20.2",
"bull": "^4.12.4",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
+14 -10
View File
@@ -11,10 +11,11 @@ import { JwtModule } from "@/modules/jwt/jwt.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { RedisService } from "@/modules/redis/redis.service";
import { BullModule } from "@nestjs/bull";
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { APP_INTERCEPTOR, RouterModule } from "@nestjs/core";
import { seconds, ThrottlerModule } from "@nestjs/throttler";
import { APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";
import { seconds, ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";
import { AppController } from "./app.controller";
@@ -26,21 +27,20 @@ import { AppController } from "./app.controller";
isGlobal: true,
load: [appConfig],
}),
RedisModule,
BullModule.forRoot({
redis: `${process.env.REDIS_URL}${process.env.NODE_ENV === "production" ? "?tls=true" : ""}`,
}),
ThrottlerModule.forRootAsync({
imports: [RedisModule],
inject: [RedisService],
useFactory: (redisService: RedisService) => ({
throttlers: [
{
name: "short",
ttl: seconds(10),
limit: 3,
},
{
name: "medium",
ttl: seconds(30),
limit: 10,
name: "long",
ttl: seconds(60), // Time to live for the long period in seconds
limit: 120, // Maximum number of requests within the long ttl
},
],
storage: new ThrottlerStorageRedisService(redisService.redis),
@@ -57,6 +57,10 @@ import { AppController } from "./app.controller";
provide: APP_INTERCEPTOR,
useClass: ResponseInterceptor,
},
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule implements NestModule {
-2
View File
@@ -30,9 +30,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
type: VersioningType.CUSTOM,
extractor: (request: unknown) => {
const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined;
console.log("asap header headerVersion", headerVersion);
if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) {
console.log("asap return header headerVersion", headerVersion);
return headerVersion;
}
return VERSION_2024_04_15;
+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;
};
@@ -16,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-0.0.2";
import { handleNewBooking } from "@calcom/platform-libraries-0.0.22";
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
describe("Bookings Endpoints", () => {
@@ -39,7 +39,7 @@ 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, UsersModule, SchedulesModule_2024_04_15],
@@ -139,7 +139,6 @@ describe("Bookings Endpoints", () => {
return request(app.getHttpServer())
.get("/v2/bookings?filters[status]=upcoming")
.then((response) => {
console.log("asap responseBody", JSON.stringify(response.body, null, 2));
const responseBody: GetBookingsOutput = response.body;
const fetchedBooking = responseBody.data.bookings[0];
@@ -240,7 +239,8 @@ describe("Bookings Endpoints", () => {
// });
// });
it("should cancel a booking", async () => {
// cancelling a booking hangs the test for some reason
it.skip("should cancel a booking", async () => {
const bookingId = createdBooking.id;
const body = {
@@ -1,11 +1,13 @@
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input";
import { MarkNoShowInput } from "@/ee/bookings/inputs/mark-no-show.input";
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
import { MarkNoShowOutput } from "@/ee/bookings/outputs/mark-no-show.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";
@@ -32,20 +34,19 @@ import { Request } from "express";
import { NextApiRequest } from "next/types";
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
import { BOOKING_READ, SUCCESS_STATUS } from "@calcom/platform-constants";
import {
getAllUserBookings,
getBookingInfo,
handleCancelBooking,
getBookingForReschedule,
} from "@calcom/platform-libraries-0.0.2";
import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants";
import {
handleNewBooking,
BookingResponse,
HttpError,
handleNewRecurringBooking,
handleInstantMeeting,
} from "@calcom/platform-libraries-0.0.2";
handleMarkNoShow,
getAllUserBookings,
getBookingInfo,
handleCancelBooking,
getBookingForReschedule,
} from "@calcom/platform-libraries-0.0.22";
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
import { ApiResponse } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
@@ -89,7 +90,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 })
@@ -149,17 +150,21 @@ export class BookingsController {
@Req() req: BookingRequest,
@Body() body: CreateBookingInput,
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse<unknown>> {
): Promise<ApiResponse<Partial<BookingResponse>>> {
const oAuthClientId = clientId?.toString();
const { orgSlug, locationUrl } = body;
req.headers["x-cal-force-slug"] = orgSlug;
try {
const booking = await handleNewBooking(
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
);
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
if (booking.userId && booking.uid && booking.startTime) {
void (await this.billingService.increaseUsageByUserId(booking.userId, {
uid: booking.uid,
startTime: booking.startTime,
fromReschedule: booking.fromReschedule,
}));
}
return {
status: SUCCESS_STATUS,
data: booking,
@@ -176,14 +181,22 @@ export class BookingsController {
@Param("bookingId") bookingId: string,
@Body() _: CancelBookingInput,
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse> {
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
const oAuthClientId = clientId?.toString();
if (bookingId) {
try {
req.body.id = parseInt(bookingId);
await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
const res = await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
if (!res.onlyRemovedAttendee) {
void (await this.billingService.cancelUsageByBookingUid(res.bookingUid));
}
return {
status: SUCCESS_STATUS,
data: {
bookingId: res.bookingId,
bookingUid: res.bookingUid,
onlyRemovedAttendee: res.onlyRemovedAttendee,
},
};
} catch (err) {
this.handleBookingErrors(err);
@@ -194,6 +207,27 @@ export class BookingsController {
throw new InternalServerErrorException("Could not cancel booking.");
}
@Post("/:bookingUid/mark-no-show")
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard)
async markNoShow(
@Body() body: MarkNoShowInput,
@Param("bookingUid") bookingUid: string
): Promise<MarkNoShowOutput> {
try {
const markNoShowResponse = await handleMarkNoShow({
bookingUid: bookingUid,
attendees: body.attendees,
noShowHost: body.noShowHost,
});
return { status: SUCCESS_STATUS, data: markNoShowResponse };
} catch (err) {
this.handleBookingErrors(err, "no-show");
}
throw new InternalServerErrorException("Could not mark no show.");
}
@Post("/recurring")
async createRecurringBooking(
@Req() req: BookingRequest,
@@ -206,7 +240,14 @@ export class BookingsController {
await this.createNextApiBookingRequest(req, oAuthClientId)
);
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
createdBookings.forEach(async (booking) => {
if (booking.userId && booking.uid && booking.startTime) {
void (await this.billingService.increaseUsageByUserId(booking.userId, {
uid: booking.uid,
startTime: booking.startTime,
}));
}
});
return {
status: SUCCESS_STATUS,
@@ -231,7 +272,15 @@ export class BookingsController {
await this.createNextApiBookingRequest(req, oAuthClientId)
);
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
if (instantMeeting.userId && instantMeeting.bookingUid) {
const now = new Date();
// add a 10 secondes delay to the usage incrementation to give some time to cancel the booking if needed
now.setSeconds(now.getSeconds() + 10);
void (await this.billingService.increaseUsageByUserId(instantMeeting.userId, {
uid: instantMeeting.bookingUid,
startTime: now,
}));
}
return {
status: SUCCESS_STATUS,
@@ -287,8 +336,14 @@ export class BookingsController {
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
private handleBookingErrors(err: Error | HttpError | unknown, type?: "recurring" | `instant`): void {
const errMsg = `Error while creating ${type ? type + " " : ""}booking.`;
private handleBookingErrors(
err: Error | HttpError | unknown,
type?: "recurring" | `instant` | "no-show"
): void {
const errMsg =
type === "no-show"
? `Error while marking no-show.`
: `Error while creating ${type ? type + " " : ""}booking.`;
if (err instanceof HttpError) {
const httpError = err as HttpError;
throw new HttpException(httpError?.message ?? errMsg, httpError?.statusCode ?? 500);
@@ -1,7 +1,7 @@
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
import type { AppsStatus } from "@calcom/platform-libraries-0.0.2";
import type { AppsStatus } from "@calcom/platform-libraries-0.0.22";
export class CreateRecurringBookingInput extends CreateBookingInput {
@IsBoolean()
@@ -0,0 +1,22 @@
import { Type } from "class-transformer";
import { IsOptional, IsArray, IsEmail, IsBoolean, ValidateNested } from "class-validator";
class Attendee {
@IsEmail()
email!: string;
@IsBoolean()
noShow!: boolean;
}
export class MarkNoShowInput {
@IsBoolean()
@IsOptional()
noShowHost?: boolean;
@ValidateNested()
@Type(() => Attendee)
@IsArray()
@IsOptional()
attendees?: Attendee[];
}
@@ -0,0 +1,45 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsString, IsEnum, IsOptional, ValidateNested, IsArray, IsEmail, IsBoolean } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Attendee {
@IsEmail()
email!: string;
@IsBoolean()
noShow!: boolean;
}
class HandleMarkNoShowData {
@IsString()
message!: string;
@IsBoolean()
@IsOptional()
noShowHost?: boolean;
@IsString()
@IsOptional()
messageKey?: string;
@ValidateNested()
@Type(() => Attendee)
@IsArray()
@IsOptional()
attendees?: Attendee[];
}
export class MarkNoShowOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: HandleMarkNoShowData,
})
@ValidateNested()
@Type(() => HandleMarkNoShowData)
data!: HandleMarkNoShowData;
}
@@ -7,6 +7,11 @@ export interface CalendarApp {
check(userId: number): Promise<ApiResponse>;
}
export interface CredentialSyncCalendarApp {
save(userId: number, userEmail: string, username: string, password: string): Promise<{ status: string }>;
check(userId: number): Promise<ApiResponse>;
}
export interface OAuthCalendarApp extends CalendarApp {
connect(authorization: string, req: Request): Promise<ApiResponse<{ authUrl: string }>>;
}
@@ -1,4 +1,6 @@
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
import { OutlookService } from "@/ee/calendars/services/outlook.service";
@@ -17,8 +19,10 @@ import { Module } from "@nestjs/common";
CalendarsService,
OutlookService,
GoogleCalendarService,
AppleCalendarService,
SelectedCalendarsRepository,
AppsRepository,
CalendarsRepository,
],
controllers: [CalendarsController],
exports: [CalendarsService],
@@ -0,0 +1,51 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { Prisma } from "@prisma/client";
const credentialForCalendarRepositorySelect = Prisma.validator<Prisma.CredentialSelect>()({
id: true,
appId: true,
type: true,
userId: true,
user: {
select: {
email: true,
},
},
teamId: true,
key: true,
invalid: true,
});
@Injectable()
export class CalendarsRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async getCalendarCredentials(credentialId: number, userId: number) {
return await this.dbRead.prisma.credential.findFirst({
where: {
id: credentialId,
userId,
},
select: {
...credentialForCalendarRepositorySelect,
app: {
select: {
slug: true,
categories: true,
dirName: true,
},
},
},
});
}
async deleteCredentials(credentialId: number) {
return await this.dbWrite.prisma.credential.delete({
where: {
id: credentialId,
},
});
}
}
@@ -1,5 +1,6 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { DeletedCalendarCredentialsOutputResponseDto } from "@/ee/calendars/outputs/delete-calendar-credentials.output";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
@@ -16,7 +17,9 @@ import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-cli
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 { CalendarsServiceMock } from "test/mocks/calendars-service-mock";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import {
GOOGLE_CALENDAR,
OFFICE_365_CALENDAR,
@@ -27,20 +30,6 @@ import { OFFICE_365_CALENDAR_ID, OFFICE_365_CALENDAR_TYPE } from "@calcom/platfo
const CLIENT_REDIRECT_URI = "http://localhost:5555";
class CalendarsServiceMock {
async getCalendars() {
return {
connectedCalendars: [
{
integration: {
type: "google_calendar",
},
},
],
};
}
}
describe("Platform Calendars Endpoints", () => {
let app: INestApplication;
@@ -66,8 +55,7 @@ describe("Platform Calendars Endpoints", () => {
.useValue({
canActivate: () => true,
})
.overrideProvider(CalendarsService)
.useClass(CalendarsServiceMock)
.compile();
app = moduleRef.createNestApplication();
@@ -85,6 +73,9 @@ describe("Platform Calendars Endpoints", () => {
accessTokenSecret = tokens.accessToken;
refreshTokenSecret = tokens.refreshToken;
await app.init();
jest
.spyOn(CalendarsService.prototype, "getCalendars")
.mockImplementation(CalendarsServiceMock.prototype.getCalendars);
});
async function createOAuthClient(organizationId: number) {
@@ -116,15 +107,16 @@ describe("Platform Calendars Endpoints", () => {
.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();
});
// TODO: Uncomment this once CI is ready to run proper Office365 tests
// 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())
@@ -200,6 +192,26 @@ describe("Platform Calendars Endpoints", () => {
.expect(200);
});
it.skip(`/POST/v2/calendars/${OFFICE_365_CALENDAR}/disconnect: it should respond with a 201 returning back the user deleted calendar credentials`, async () => {
const body = {
id: 10,
};
return request(app.getHttpServer())
.post(`/v2/calendars/${OFFICE_365_CALENDAR}/disconnect`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: Promise<DeletedCalendarCredentialsOutputResponseDto> = response.body;
expect((await responseBody).status).toEqual(SUCCESS_STATUS);
expect((await responseBody).data).toBeDefined();
expect((await responseBody).data.id).toEqual(body.id);
expect((await responseBody).data.userId).toEqual(user.id);
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -1,12 +1,19 @@
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { DeleteCalendarCredentialsInputBodyDto } from "@/ee/calendars/input/delete-calendar-credentials.input";
import { GetBusyTimesOutput } from "@/ee/calendars/outputs/busy-times.output";
import { ConnectedCalendarsOutput } from "@/ee/calendars/outputs/connected-calendars.output";
import {
DeletedCalendarCredentialsOutputResponseDto,
DeletedCalendarCredentialsOutputDto,
} from "@/ee/calendars/outputs/delete-calendar-credentials.output";
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
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 { 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 {
@@ -21,13 +28,23 @@ import {
Headers,
Redirect,
BadRequestException,
Post,
Body,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { plainToClass } from "class-transformer";
import { Request } from "express";
import { z } from "zod";
import { APPS_READ } from "@calcom/platform-constants";
import { SUCCESS_STATUS, CALENDARS, GOOGLE_CALENDAR, OFFICE_365_CALENDAR } from "@calcom/platform-constants";
import {
SUCCESS_STATUS,
CALENDARS,
GOOGLE_CALENDAR,
OFFICE_365_CALENDAR,
APPLE_CALENDAR,
} from "@calcom/platform-constants";
import { ApiResponse, CalendarBusyTimesInput } from "@calcom/platform-types";
@Controller({
@@ -39,10 +56,12 @@ export class CalendarsController {
constructor(
private readonly calendarsService: CalendarsService,
private readonly outlookService: OutlookService,
private readonly googleCalendarService: GoogleCalendarService
private readonly googleCalendarService: GoogleCalendarService,
private readonly appleCalendarService: AppleCalendarService,
private readonly calendarsRepository: CalendarsRepository
) {}
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
@Get("/busy-times")
async getBusyTimes(
@Query() queryParams: CalendarBusyTimesInput,
@@ -71,7 +90,7 @@ export class CalendarsController {
}
@Get("/")
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
const calendars = await this.calendarsService.getCalendars(userId);
@@ -81,7 +100,7 @@ export class CalendarsController {
};
}
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
@Get("/:calendar/connect")
@HttpCode(HttpStatus.OK)
async redirect(
@@ -133,16 +152,18 @@ export class CalendarsController {
}
}
@Get("/:calendar/check")
@HttpCode(HttpStatus.OK)
@UseGuards(AccessTokenGuard, PermissionsGuard)
@Permissions([APPS_READ])
async check(@GetUser("id") userId: number, @Param("calendar") calendar: string): Promise<ApiResponse> {
@UseGuards(ApiAuthGuard)
@Post("/:calendar/credentials")
async syncCredentials(
@GetUser() user: User,
@Param("calendar") calendar: string,
@Body() body: { username: string; password: string }
): Promise<{ status: string }> {
const { username, password } = body;
switch (calendar) {
case OFFICE_365_CALENDAR:
return await this.outlookService.check(userId);
case GOOGLE_CALENDAR:
return await this.googleCalendarService.check(userId);
case APPLE_CALENDAR:
return await this.appleCalendarService.save(user.id, user.email, username, password);
default:
throw new BadRequestException(
"Invalid calendar type, available calendars are: ",
@@ -150,4 +171,49 @@ export class CalendarsController {
);
}
}
@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);
case APPLE_CALENDAR:
return await this.appleCalendarService.check(userId);
default:
throw new BadRequestException(
"Invalid calendar type, available calendars are: ",
CALENDARS.join(", ")
);
}
}
@UseGuards(ApiAuthGuard)
@Post("/:calendar/disconnect")
@HttpCode(HttpStatus.OK)
async deleteCalendarCredentials(
@Param("calendar") calendar: string,
@Body() body: DeleteCalendarCredentialsInputBodyDto,
@GetUser() user: UserWithProfile
): Promise<DeletedCalendarCredentialsOutputResponseDto> {
const { id: credentialId } = body;
await this.calendarsService.checkCalendarCredentials(credentialId, user.id);
const { id, type, userId, teamId, appId, invalid } = await this.calendarsRepository.deleteCredentials(
credentialId
);
return {
status: SUCCESS_STATUS,
data: plainToClass(
DeletedCalendarCredentialsOutputDto,
{ id, type, userId, teamId, appId, invalid },
{ strategy: "excludeAll" }
),
};
}
}
@@ -0,0 +1,15 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose } from "class-transformer";
import { IsInt } from "class-validator";
export class DeleteCalendarCredentialsInputBodyDto {
@IsInt()
@Expose()
@ApiProperty({
example: 10,
description: "Credential ID of the calendar to delete, as returned by the /calendars endpoint",
type: "integer",
required: true,
})
readonly id!: number;
}
@@ -0,0 +1,42 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsString, ValidateNested, IsEnum, IsInt, IsBoolean } from "class-validator";
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
export class DeletedCalendarCredentialsOutputDto {
@IsInt()
@Expose()
readonly id!: number;
@IsString()
@Expose()
readonly type!: string;
@IsInt()
@Expose()
readonly userId!: number | null;
@IsInt()
@Expose()
readonly teamId!: number | null;
@IsString()
@Expose()
readonly appId!: string | null;
@IsBoolean()
@Expose()
readonly invalid!: boolean | null;
}
export class DeletedCalendarCredentialsOutputResponseDto {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@Expose()
@ValidateNested()
@Type(() => DeletedCalendarCredentialsOutputDto)
data!: DeletedCalendarCredentialsOutputDto;
}
@@ -0,0 +1,92 @@
import { CredentialSyncCalendarApp } from "@/ee/calendars/calendars.interface";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { BadRequestException, UnauthorizedException } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { SUCCESS_STATUS, APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries-0.0.22";
@Injectable()
export class AppleCalendarService implements CredentialSyncCalendarApp {
constructor(
private readonly calendarsService: CalendarsService,
private readonly credentialRepository: CredentialsRepository
) {}
async save(
userId: number,
userEmail: string,
username: string,
password: string
): Promise<{ status: string }> {
return await this.saveCalendarCredentials(userId, userEmail, username, password);
}
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
return await this.checkIfCalendarConnected(userId);
}
async checkIfCalendarConnected(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
const appleCalendarCredentials = await this.credentialRepository.getByTypeAndUserId(
APPLE_CALENDAR_TYPE,
userId
);
if (!appleCalendarCredentials) {
throw new BadRequestException("Credentials for apple calendar not found.");
}
if (appleCalendarCredentials.invalid) {
throw new BadRequestException("Invalid apple calendar credentials.");
}
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
const appleCalendar = connectedCalendars.find(
(cal: { integration: { type: string } }) => cal.integration.type === APPLE_CALENDAR_TYPE
);
if (!appleCalendar) {
throw new UnauthorizedException("Apple calendar not connected.");
}
if (appleCalendar.error?.message) {
throw new UnauthorizedException(appleCalendar.error?.message);
}
return {
status: SUCCESS_STATUS,
};
}
async saveCalendarCredentials(userId: number, userEmail: string, username: string, password: string) {
if (username.length <= 1 || password.length <= 1)
throw new BadRequestException(`Username or password cannot be empty`);
const data = {
type: APPLE_CALENDAR_TYPE,
key: symmetricEncrypt(
JSON.stringify({ username, password }),
process.env.CALENDSO_ENCRYPTION_KEY || ""
),
userId: userId,
teamId: null,
appId: APPLE_CALENDAR_ID,
invalid: false,
};
try {
const dav = new CalendarService({
id: 0,
...data,
user: { email: userEmail },
});
await dav?.listCalendars();
await this.credentialRepository.createAppCredential(APPLE_CALENDAR_TYPE, data.key, userId);
} catch (reason) {
throw new BadRequestException(`Could not add this apple calendar account: ${reason}`);
}
return {
status: SUCCESS_STATUS,
};
}
}
@@ -1,3 +1,4 @@
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import {
CredentialsRepository,
@@ -17,8 +18,7 @@ import { User } from "@prisma/client";
import { DateTime } from "luxon";
import { z } from "zod";
import { getConnectedDestinationCalendars } from "@calcom/platform-libraries-0.0.2";
import { getBusyCalendarTimes } from "@calcom/platform-libraries-0.0.2";
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries-0.0.22";
import { Calendar } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
@@ -30,6 +30,7 @@ export class CalendarsService {
private readonly usersRepository: UsersRepository,
private readonly credentialsRepository: CredentialsRepository,
private readonly appsRepository: AppsRepository,
private readonly calendarsRepository: CalendarsRepository,
private readonly dbRead: PrismaReadService,
private readonly dbWrite: PrismaWriteService,
private readonly config: ConfigService
@@ -137,4 +138,11 @@ export class CalendarsService {
return { client_id, client_secret };
}
async checkCalendarCredentials(credentialId: number, userId: number) {
const credential = await this.calendarsRepository.getCalendarCredentials(credentialId, userId);
if (!credential) {
throw new NotFoundException("Calendar credentials not found");
}
}
}
@@ -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-0.0.2";
} from "@calcom/platform-libraries-0.0.22";
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) => {
@@ -207,7 +214,7 @@ 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 = {
const body: UpdateEventTypeInput_2024_04_15 = {
locations,
};
@@ -217,7 +224,7 @@ describe("Event types Endpoints", () => {
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,
};
@@ -266,7 +273,7 @@ describe("Event types Endpoints", () => {
},
];
const body: UpdateEventTypeInput = {
const body: UpdateEventTypeInput_2024_04_15 = {
bookingFields,
};
@@ -288,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);
@@ -305,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);
@@ -323,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);
@@ -340,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);
@@ -359,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);
@@ -375,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,19 +1,22 @@
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
import { EventTypeIdParams } from "@/ee/event-types/inputs/event-type-id.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 { API_VERSIONS_VALUES } from "@/lib/api-versions";
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";
@@ -36,27 +39,27 @@ import {
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-0.0.2";
import { getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
import { PrismaClient } from "@calcom/prisma";
@Controller({
path: "/v2/event-types",
version: API_VERSIONS_VALUES,
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);
@@ -69,11 +72,9 @@ export class EventTypesController {
@Get("/:eventTypeId")
@Permissions([EVENT_TYPE_READ])
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
async getEventType(
@Param() params: EventTypeIdParams,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@GetUser() user: UserWithProfile
): Promise<GetEventTypeOutput> {
const eventType = await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId));
@@ -90,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,
@@ -109,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(
@@ -146,12 +147,12 @@ export class EventTypesController {
@Patch("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
@HttpCode(HttpStatus.OK)
async updateEventType(
@Param() params: EventTypeIdParams,
@Param() params: EventTypeIdParams_2024_04_15,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@Body() body: UpdateEventTypeInput,
@Body() body: UpdateEventTypeInput_2024_04_15,
@GetUser() user: UserWithProfile
): Promise<UpdateEventTypeOutput> {
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
@@ -164,9 +165,9 @@ export class EventTypesController {
@Delete("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(AccessTokenGuard)
@UseGuards(ApiAuthGuard)
async deleteEventType(
@Param() params: EventTypeIdParams,
@Param() params: EventTypeIdParams_2024_04_15,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@GetUser("id") userId: number
): Promise<DeleteEventTypeOutput> {
@@ -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-0.0.2";
import { getEventTypeById } from "@calcom/platform-libraries-0.0.22";
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()
@@ -1,6 +1,6 @@
import { IsNumberString } from "class-validator";
export class EventTypeIdParams {
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,36 @@
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-0.0.2";
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries-0.0.2";
import {
createEventType,
updateEventType,
EventTypesPublic,
getEventTypesPublic,
} from "@calcom/platform-libraries-0.0.22";
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 +45,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.");
@@ -53,6 +60,7 @@ export class EventTypesService {
const profileId = user.movedToProfile?.id || null;
return {
id: user.id,
role: user.role,
organizationId: user.organizationId,
organization: { isOrgAdmin },
profile: { id: profileId },
@@ -114,7 +122,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,383 @@
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, Schedule } 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 { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { 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;
let schedulesRepostoryFixture: SchedulesRepositoryFixture;
const userEmail = "event-types-test-e2e@api.com";
const falseTestEmail = "false-event-types@api.com";
const name = "bob-the-builder";
const username = name;
let eventType: EventTypeOutput_2024_06_14;
let user: User;
let falseTestUser: User;
let firstSchedule: Schedule;
let secondSchedule: Schedule;
let falseTestSchedule: Schedule;
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);
schedulesRepostoryFixture = new SchedulesRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization" });
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
name,
username,
});
falseTestUser = await userRepositoryFixture.create({
email: falseTestEmail,
name: "false-test",
username: falseTestEmail,
});
firstSchedule = await schedulesRepostoryFixture.create({
userId: user.id,
name: "work",
timeZone: "Europe/Rome",
});
secondSchedule = await schedulesRepostoryFixture.create({
userId: user.id,
name: "chill",
timeZone: "Europe/Rome",
});
falseTestSchedule = await schedulesRepostoryFixture.create({
userId: falseTestUser.id,
name: "work",
timeZone: "Europe/Rome",
});
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["redirect-uri"],
permissions: 32,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it("should be defined", () => {
expect(oauthClientRepositoryFixture).toBeDefined();
expect(userRepositoryFixture).toBeDefined();
expect(oAuthClient).toBeDefined();
expect(user).toBeDefined();
});
it("should not allow creating an event type with schedule user does not own", async () => {
const scheduleId = falseTestSchedule.id;
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"],
},
],
scheduleId,
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(404);
});
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"],
},
],
scheduleId: firstSchedule.id,
};
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);
expect(createdEventType.scheduleId).toEqual(firstSchedule.id);
eventType = responseBody.data;
});
});
it("should update event type", async () => {
const newTitle = "Coding class in Italian!";
const body: UpdateEventTypeInput_2024_06_14 = {
title: newTitle,
scheduleId: secondSchedule.id,
};
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);
expect(updatedEventType.scheduleId).toEqual(secondSchedule.id);
eventType.title = newTitle;
eventType.scheduleId = secondSchedule.id;
});
});
it("should not allow to update event type with scheduleId user does not own", async () => {
const body: UpdateEventTypeInput_2024_06_14 = {
scheduleId: falseTestSchedule.id,
};
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(404);
});
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 event type", 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
}
try {
await userRepositoryFixture.delete(falseTestUser.id);
} catch (e) {
// User might have been deleted by the test
}
await app.close();
});
});
});
@@ -0,0 +1,127 @@
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("/")
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,34 @@
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 { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
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,
SchedulesRepository_2024_06_11,
],
controllers: [EventTypesController_2024_06_14],
exports: [
EventTypesService_2024_06_14,
EventTypesRepository_2024_06_14,
InputEventTypesService_2024_06_14,
OutputEventTypesService_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.22";
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,274 @@
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 { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
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-0.0.22";
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries-0.0.22";
import { dynamicEvent } from "@calcom/platform-libraries-0.0.22";
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,
private readonly schedulesRepository: SchedulesRepository_2024_06_11
) {}
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.");
}
await this.checkUserOwnsSchedule(userId, body.scheduleId);
}
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,
role: user.role,
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) {
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
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, scheduleId: number | undefined) {
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 });
await this.checkUserOwnsSchedule(userId, scheduleId);
}
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}`);
}
}
async checkUserOwnsSchedule(userId: number, scheduleId: number | null | undefined) {
if (!scheduleId) {
return;
}
const schedule = await this.schedulesRepository.getScheduleByIdAndUserId(scheduleId, userId);
if (!schedule) {
throw new NotFoundException(`User with ID=${userId} does not own schedule with ID=${scheduleId}`);
}
}
}

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