Compare commits

...
241 Commits
Author SHA1 Message Date
Alex van Andel 4a914c2dfb chore: Release v4.5.3 (#16728)
release.yml / release (push) Canceled after 0s
2024-09-20 16:27:25 +01:00
Joe Au-Yeung 62d579d03d Default to connected Salesforce account as owner (#16747) 2024-09-20 15:27:05 +00:00
Morgan 74bed31edc chore: improve api-v2 rate limiting (#16746) 2024-09-20 15:15:48 +00:00
Alex van Andel 97f213f260 revert: be7d45afe2 (#16742) 2024-09-20 15:01:49 +00:00
Carina WollendorferandCarinaWolli b84cdc0d45 fix: bookings ilmits errror (#16739)
* use booker timezone

* add test

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-09-20 13:35:12 +00:00
Syed Ali Shahbaz 8cb5a7080b feat: Add EXPAND accepting "team" in API V1 Bookings (#16736)
* add expand

* add expand to /[id]/_get

* add test

* fix test import

* --

* --

* fix test

* fix test

* revert unintentional commit
2024-09-20 11:46:26 +00:00
Lauris Skraucis 11f0d48d0b feat: onBeforeUpdate AvailabilitySettings prop (#16737) 2024-09-20 11:42:10 +00:00
Syed Ali Shahbaz c4a1382982 chore: add granular logging to the logger (#16734)
* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update turbo.json

* Update trpc-provider.tsx

* Update logger.ts

* update prisma index

* Update trpc.ts

* Update trpc-provider.tsx

* Update trpc.ts

* Update .env.example

* Update logger.ts
2024-09-20 09:06:37 +00:00
Calcom BotandReplexica e0922c5ee0 feat: update translations (#16726)
release.yml / release (push) Canceled after 0s
Co-authored-by: Replexica <support@replexica.com>
2024-09-19 16:58:47 +00:00
Benny JooandJoe Au-Yeung f8dc7a115a chore: App Router - remove references to /pages for remaining pages in /auth, /insights, /d, signup and add missing default /future page (#16589)
* remove references to /pages for sso, setup, signin pages in /auth

* remove references to pages for insights

* remove references to pages for d

* remove references to pages for signup

* add page for /future index page

* fix routing-forms

* fix

* add missing defaults

* use getServerSessionForAppDir instead

* fix apps/[slug]/[...pages]

* fix metadata in apps/slug/pages

* refactor

* refactor

* remove duplicate code for PageProps

* remove references to pages for /reschedule

* fix

* fix routing forms

* type fix

* fix routing forms again

* revert changes for app/slug/pages

* revert

* revert changes in yarn lock

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-09-19 15:06:51 +00:00
sean-brydonandPeer Richelsen a60aa1bf6c add create license key to admin tab if IS_CALCOM (#16721)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-09-19 13:09:14 +00:00
Hariom Balhara bc203d7439 Release latest embed (#16713) 2024-09-19 11:02:17 +00:00
Udit Takkar d82847035f fix: type error on booking cancelled page (#16716) 2024-09-19 10:13:16 +00:00
Hariom Balhara 6d133c26f8 Narrow argument type to the specific event being listened to (#16714) 2024-09-19 09:42:32 +00:00
Benny Joo 3ae53a4b6e chore: App router - Fix errors in settings pages & add remaining pages (#16648)
* fix settings/admin/page

* remove references to pages for settings/teams

* remove double layouts

* move pages that do not need layout out of (settings) group

* fix

* fix

* fix

* fix

* add sso page and fix layouts
2024-09-19 09:59:32 +01:00
Lauris Skraucis 63339d8c01 fix: connect apple calendar atom nested <button>'s (#16715) 2024-09-19 08:49:21 +00:00
Lauris SkraucisandMorgan 44410c87f0 docs: v2 schedules and event-types swagger (#16608)
* docs: schedules

* docs: wip event-types

* docs:event-types

* docs:event-types

* Delete yarn.lock

* Revert "Delete yarn.lock"

This reverts commit bb3eb23508c0fa2e216ba7bcb97756215f889daa.

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-09-19 10:00:17 +03:00
Omar López c016241004 chore: removes unnecessary prisma env warnings (#16708)
* chore: removes unnecessary prisma env warnings

* Revert lockfile

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

* Update yarn lock

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

---------

Signed-off-by: zomars <zomars@me.com>
2024-09-19 07:32:23 +05:30
Benny Joo 019ef6de2a chore: App router migration - (/bookings, /booking) Move getServerSideProps/getStaticProps to /lib (#16458)
* booking: Move getServerSideProps to /lib

* remove duplicate

* bookings: Move getStaticProps to /lib

* fix type check

* replace setHeader with a server action call in withEmbedSSR

* Add env variables for booking
2024-09-18 19:09:57 -04:00
Omar López a2b064b269 feat: per user and team feature flags (#16040) 2024-09-18 21:27:47 +00:00
abb4fd307f refactor: event setup tab web wrapper (#16672)
* chore: EventTypeWebWrapper base pr

* fixup! chore: EventTypeWebWrapper base pr

* fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

* wip

* fix: error

* Event type atom wrapper handles app dir

* fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

* fixup! fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

* remove console log

* refactor: event-type-atom (setup tab)

* review comments

* update: better naming

* fixup! review comments

* revert: defaultMenuIsOpen --will create a separate PR

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-09-18 20:14:23 +03:00
Peer Richelsen a9ebb0d160 feat: Retell AI (#16706)
* added retell ai to app store

* added retell ai
2024-09-18 17:08:01 +00:00
sean-brydon 1058c0e171 fix(eslint): Adds eslint-ignore-patterns to eslint config (#16701) 2024-09-18 15:17:52 +00:00
Anik Dhabal Babu 021e5f3833 fix: fixed more flaky e2e tests (round 2) (#16660)
* fix: fixed more flaky e2e tests (round 2)

* update

* fix and update

* update
2024-09-18 23:25:44 +09:00
Lauris Skraucis 5b5a42e72d refactor: v2 and atoms booking fields (#16685)
* refactor: define inputs and outputs by separating default and custom fields

* refactor: event type api <-> internal transformers

* refactor: v2 use refactored libraries to transform input and output booking fields

* refactor: dont allow custom slugs with default reserved slugs

* refactor: input service store only specific default system fields

* refactor: atoms display only specific default system fields

* refactor: delete unused system field constants

* fix: tests

* fix: old event types

* fix: imports

* fix: import

* chore: release libraries and use v2 with them

* fix: unit tests

* refactor: push email in case of missing phone

* refactor: satisfies check for event-type locations
2024-09-18 13:59:31 +02:00
MorganandSomay Chauhan 2b43d9a716 chore: EventTypeWebWrapper base pr (#16550)
* chore: EventTypeWebWrapper base pr

* fixup! chore: EventTypeWebWrapper base pr

* fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

* wip

* fix: error

* Event type atom wrapper handles app dir

* fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

* fixup! fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

* remove console log

* review comments

* fixup! review comments

* fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-09-18 12:26:04 +03:00
Omar López 3a5325f2a8 fix: new user detection for dub analytics (#16695) 2024-09-17 19:19:25 -07:00
Calcom BotandReplexica 5ad73607f0 feat: update translations (#16693)
Co-authored-by: Replexica <support@replexica.com>
2024-09-17 23:21:10 +00:00
Carina Wollendorfer 5067f0dd16 fix: dates in ooo booking redirect email (#16692) 2024-09-17 22:28:40 +00:00
Keith Williams a53e697baa chore: v4.5.1 (#16690)
release.yml / release (push) Canceled after 0s
2024-09-18 04:05:16 +09:00
sean-brydon e7415fe6e2 feat: update vitest version v0.34 to V2.11 (#16676)
* update version

* fix button to match new spec

* migrate tests to objectContaining

* fix tests

* update RTL

* yarn.lock update

* bump fetch mock to latest vitest version

* update deep mock to use vitest 2.0

* fix mock prisma type cast

* meet base test fn singature for V2

* remove console.log from next test
2024-09-17 19:09:55 +01:00
sean-brydonandJoe Au-Yeung 562309d24b fix: add optimistic UI for deleting member on org user table (#16662)
* add optimistic UI for deleting member

* Update packages/features/users/components/UserTable/DeleteMemberModal.tsx

* Remove previous Value

* Update packages/features/users/components/UserTable/DeleteMemberModal.tsx

* Update packages/features/users/components/UserTable/DeleteMemberModal.tsx

* fix onError

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-09-17 10:19:45 +00:00
Lauris Skraucis 3900c55204 fix: make connect calendar atoms client side (#16677)
* fix: make connect calendar atoms client side

* delete unrelated file
2024-09-17 10:16:12 +00:00
Morgan b3be9a93e0 chore: expose metadata on platform booker atom (#16673) 2024-09-17 10:12:30 +00:00
d27ccd6f44 feat: org team app install (#15704)
* feat: remove dropdown from app-store and redirect to new-app-install-flow

* removed extra code

* fix: account select loading issue

* removed getPaymentCredential (unused)

* fix: only redirect after the app has been added to all the event-types

* remove overflowIndicatorStyles

* refactor getUserAdminTeams

* send teamId instead of id

* seperate locations component

* for conferencing apps skip select account page

* refactor InstallAppButtonChild

* installing conferencing apps shows locations dropdown in configure step

* send location data to the handler

* send location data to the handler
* add the newly installed to the locations dropdown (prefillLocation)

* fix: type errors

* fix: handle es-lint errors

* fix: app is added again on submit

* only add app if not already added

* fix: type erros

* filter out managed events for now

* fix: show installed count badge

* remove 2 toast message

* feat: added tests for conferencing apps

* fix: loading indicator

* Update apps/web/pages/apps/installation/[[...step]].tsx

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

* Update apps/web/pages/apps/installation/[[...step]].tsx

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

* move `getUserAdminTeams` to `UserRepository`

* fix: zoom and jelly not redirecting to the new app install flow

* fix: typo

* add `installableOnTeams` prop

* split `configureStepCard` into smaller components

* fix: step count

* fix: show loading indicator until the new page is loaded

* fix: organizer_default_conferencing_app not visible in a team

* Introduce QueryClient to app card tests

* fix: typo

* add installableOnTeams prop

* fix: tests

* seated events shouldn't have multiple locations #15174

* fix: o365 callback

* fix: office365_video not redirecting to event-types step

* Revert "fix: o365 callback"

This reverts commit bba841035ea34f8c31201b64c77221b8d8c3e626.

* add TEAM_SELECT_STEP

* add apps to orgs and their sub-teams initial commit

* undo team select step

* small ui fix

* fix: wrong step numbers

* fix: dont allow app installation without cretendialId

* fix: don't show acme team as it cannot have any events

* refactor useAddAppMutation

* removed console.log

* added comment

* added comment

* move locationOptions from getServerSideProps a trpc query

* fix: failing tests

* Update conferencingApps.e2e.ts

* refactor useAddAppMutation

* refactor useAddAppMutation 2

* fix: test failing

* fix: unit test

* Revert "fix: unit test"

This reverts commit 6d74032211d094478c6d7cf9aedbce696dfb768d.

* fix: failing test

* Increase test timeout for conferencing app tests

* fix: write separate tests for each conferencing app to prevent hitting 6000 ms timeout

* improved tests naming

* fix: correct message and translation key #15657

* fix: write separate tests for each analytics app to prevent hitting 6000 ms timeout

* fix: analytics apps test

* attempt to fix failing tests

* fix typo

* refactor

* update: replace text-gray with text-stuble

- works with light mode too

* update: use userRepository.getUserAdminTeams

* Merge branch 'main' into feat/org-team-app-install-2

* fix: after merge conflict from app router migration

* remove consoles

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-09-17 04:40:56 +00:00
Benny Joo f86a5db843 chore: App Router Migration, add metadata to getting-started, extract into /modules and add method to UserRepository (#16449)
* getting-started: extract into /modules

* fix

* fix type-check

* add findByIdWithOptionalSelect to UserRepository

* fix import error

* fix type check

* add metadata

* fix
2024-09-17 02:42:19 +00:00
Steven Tey 4cbf9ec9ab Update dclid → dub_id (#16670) 2024-09-16 22:53:05 +00:00
Benny Joo aa3c91ebd6 chore: App router - add missing metadata for /[user] pages (#16516) 2024-09-16 10:42:00 -04:00
Rajiv Sahal 071c899ea6 fix: overlay calendar view for Booker atom (#16642)
* overlay calendar needs to be false when in month_view

* fixup
2024-09-16 14:39:18 +00:00
sean-brydon 32b2e83564 fix: fix org check on team slider (#16659) 2024-09-16 14:25:10 +00:00
Curious GeorgeandAnik Dhabal Babu 7ff6d6559a Update callback.ts to work with AU location (#16664)
as per https://accounts.zoho.com/oauth/serverinfo, the server location for au is .com.au instead of .au

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-09-16 12:49:17 +00:00
2572a5cb93 fixes multiselect required check (#16653)
Co-authored-by: Ashutosh Sangwan <ashutoshsangwan@Ashutoshs-MacBook-Pro.local>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-09-16 10:31:33 +00:00
Syed Ali Shahbaz 45e0f87e45 fix: add logger to util to debug v1 API slot (#16657)
* Update util.ts

* fix init

* add logger to repository_user
2024-09-16 08:29:58 +00:00
NETRAKAMAL BARUA 6e875fccbe fix: verifyCodeDialog ui (#16655) 2024-09-16 03:32:31 +00:00
Benny Joo 689e17bcf6 fix (#16632) 2024-09-15 20:23:37 -03:00
Keith Williams 584f9c6e93 chore: v4.5.0 (#16650)
release.yml / release (push) Canceled after 0s
2024-09-15 22:30:20 +09:00
Benny Joo 70348c6f64 fix: Add new api-key CTA button is missing in settings/developer/api-keys (#16649)
* Add new api-key button is missing

* fix type check
2024-09-15 22:09:03 +09:00
747591fded refactor: remove networkidle waiters (#16483)
* refactor: remove networkidle waiters

* Update out-of-office.e2e.ts

* Update auth-index.e2e.ts

* WIP

* Update booking-limits.e2e.ts

* fix-conflicts

* fix failed e2e tests

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
2024-09-15 12:25:48 +00:00
Anik Dhabal Babu 9bfd933699 fix: managed eventype is not deleted to the assigned user when team is deleted (#16621) 2024-09-13 23:40:25 +01:00
Rajiv SahalandAnik Dhabal Babu a775e42e85 fix: prevent creating multiple platform orgs (#16629)
* set error messages to know whats wrong

* trying to find if org with same owner already exists

* ensure a user cannot create more than one platform team

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-09-13 23:37:48 +01:00
Peer Richelsen e3e2bc804a chore: added IS_EUROPE for signup (#16639) 2024-09-13 22:17:24 +00:00
sean-brydon b84099aec0 fix: use correct redirect url for google oauth when using overlay calendar (#16623) 2024-09-13 18:14:33 +00:00
Morgan 073d34f893 chore: rewrite imports of prisma enums to @calcom/prisma/enums (#16633) 2024-09-13 17:45:59 +00:00
Rajiv SahalandPeer Richelsen d72900d164 hide platform tab from navigation when team not setup (#16630)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-09-13 17:17:44 +00:00
Syed Ali ShahbazandKeith Williams e3b478f5b1 fix: get slot logic for v1 API (#16620)
* fix faulty slug match

* Allow team event slot

* pass it to getSchedule

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-09-13 16:34:03 +00:00
a5191d1e5a chore: App-router-migration(/auth/verify) add missing metadata / page & extract components into /module (#16444)
* create StripeRepository and simplify code

* verify-email-change: extract getServerSideProps

* add verify-email-change to app router

* auth/verify: Add metadata in app router

* auth/verify: extract client component into /modules

* fix type checks for stripe

* fix stripe session handler

* improve getServerSideProps for verify-email-change

* add title to verify-email-change page metadata

* add title to verify page metadata

* refactor

* add safety

* fix

* remove references to pages for verify-email

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-09-13 16:13:33 +00:00
Benny Joo ea21cd5826 fix: make admin role unnecessary for future-opt-in API (#16628) 2024-09-13 16:01:30 +00:00
sean-brydon e78c8738fb feat: create loading state for copy referral link from dub.co (#16626)
* create loading state for copy referal

* Update packages/features/shell/Shell.tsx
2024-09-14 00:03:05 +09:00
Calcom BotandReplexica 47314bae83 feat: update translations (#16616)
Co-authored-by: Replexica <support@replexica.com>
2024-09-13 14:37:39 +00:00
fd8afbc418 chore: Add Prisma extension to block deleteMany/updateMany with undefined 'in' (#15721)
* chore: Add Prisma extension to block updateMany with undefined 'in'

* Fix type

* types

* Added check for deleteMany as well

* Improved in check

* type

* Updated check to only check undefined and not isArray

* Renamed the guard

* another rename

* Changed undefined check to use typeof

* Types

* Added check

* Update packages/prisma/extensions/disallow-undefined-in-delete-update-many.ts

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

* Added unit tests

* Added a good case

* Rename

* Rename fix

* Types

* Temporarily disabling the extension for testing

* Added back the extension

* Trying to also log to see if there are swallowed errors

* Update

* removed unused params

* removed console.log

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-09-13 22:52:06 +09:00
6e7012757f feat: replace team list view with org list view UI and fix team view user availability (#15344)
* feat: replace team list view with org list view UI and fix team view user availability

* update

* update2

* some changes

* Remove old team availability modal - fix flex direction

* fix

* fix type error

* update

* update

* fix type error

* update

* upfate edit sheet

* final update

* fix flaky e2e

* fix e2e

* typo

* profile update flow

* fix type error and refactor

* small update

* NIT

* update

* chore: fix types

* update

---------

Co-authored-by: sean-brydon <sean@cal.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-09-13 19:06:40 +05:30
sean-brydon 933eb0d057 remove teams layout (#16624) 2024-09-13 12:39:42 +00:00
Omar López fcb79888f3 fix: async clipboard support in Safari (#16619) 2024-09-13 09:27:28 +01:00
111e2f30c6 feat: overlay calendar for Booker atom (#16551)
* add prop to see if overlay calendar is enabled

* fix merge conflicts

* overlay calendar enabled prop

* cleanup

* hide toggle group if platform

* remove isPlatform conditional for overlay calendar

* dont display overlay calendar continue modal for platform

* fix images not appearing for platform

* update booker store

* update booker

* update booker

* shifting variables from booker store to useOverlayCalendar store

* update booker platform wrapper

* update booking page view

* revert changes to overlay calendar store

* update typings

* update toggle set value

* update handler to toggle connected calendar

* fixup

* update booker layout

* fixup! Merge branch 'main' into overlay-calendar-for-booker-atom

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-09-13 08:13:20 +00:00
Steven Teyandzomars f2b3e620b4 fix: Show toast after copy referral link + change icon to "gift" (#16618)
Co-authored-by: zomars <zomars@me.com>
2024-09-12 22:06:21 +00:00
Benny Joo d9133cb395 chore: App router migration - Create/Use EventRepository and Extract /team/**/* and /teams/* into /modules from /pages (#16453) 2024-09-12 18:58:08 -03:00
Omar López 1722edbd36 chore: v4.4.9 (#16617)
release.yml / release (push) Canceled after 0s
2024-09-12 20:08:07 +00:00
Amit Sharma e68ab3b123 fix: prevent linking duplicate apple calendars (#16539)
* fix: prevent linking duplicate apple calendars

* fix: update when new password

* revert unnecessary change

* fix: use array.find
2024-09-13 01:08:38 +05:30
cf16cd3ed2 feat: Implement Dub.co for conversion tracking (#16165)
* FEAT: Implement Dub.co for conversion tracking

* add link creation, @vercel/functions

* use refer.cal.com/:username instead of go.cal.com/r/:username

* sale conversion tracking

* add copy referral link

* limit to IS_CALCOM only

* Update yarn.lock

* fix DubAnalytics

* use workaround for isNewUser

* pass req to getOptions

* Update next-auth-options.ts

* fix ts errors

* only show DubAnalytics outside EU

* add Dub Analytics to /signup

* use WEBSITE_URL instead

* on-demand generate links

* add migration

* add check for existing link + change fetch method to poast

* remove refer.cal.com from PoweredByCal

* limit DubAnalytics to /signup only

* simplify generate-referral-link

* restore yarn.lock

* add yarn with dub sdk in

* add yarn with dub sdk in

* yarn

* Update yarn.lock

---------

Co-authored-by: sean <sean@brydon.io>
Co-authored-by: Peer Richelsen <peer@cal.com>
2024-09-12 12:33:10 -07:00
Keith Williams f4bc7d2ea4 chore: Remove PR reviewed automation (#16615) 2024-09-13 03:25:10 +09:00
Amit Sharma f5f7242f02 fix: use current user email (#16577)
* fix: use current user email

* dont fallback to email param
2024-09-12 16:32:49 +00:00
8f5ac26bb1 fix: profile page flickering and refactor (#16555)
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-09-12 10:12:54 -04:00
Calcom BotandReplexica b983adb16a feat: update translations (#16603)
Co-authored-by: Replexica <support@replexica.com>
2024-09-12 14:06:47 +00:00
Abhijeet SinghandAmit Sharma 8ce018995a fix: (managed-event-booking): slug conflict handling (#16540)
* fix(managed-event-booking): slug conflict handling

* refactor logic

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-09-12 10:05:05 -04:00
Maximilian FranzkeandAnik Dhabal Babu f11086e9f8 fix(ally): button (#16588)
The close button would actually need to be a `button` instead of a `span` HTML element to be both screenreader and keyboard user accessible, as well as having an accessible label.

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-09-12 13:29:27 +05:30
Rajiv SahalandMorgan 91f5d49cd4 feat: update v2 calendar services logic to handle multiple calendars (#16478)
* update calendar logic to handle multiple calendars

* add method to link calendar to credential

* remove logs

* update platform libraries

* update calendar logic to handle multiple calendars

* update credentials repository

* add selected calendars repository to destination calendars module

* add method to get user selected calendars and rename fn

* chore: PR feedback

* resolve merge conflicts

* fixup

* make sure we cover all edge cases for apple calendar

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-09-12 06:44:01 +00:00
Benny Joo 55e1e0fa3d chore: Remove passing optional prisma select prop to Repository classes (#16600) 2024-09-11 18:14:19 -07:00
4a59841b02 feat: booking with phone number (#14461)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-09-11 17:56:04 -07:00
Benny Joo 74a9f3c19e hotfix: availability/[schedule] (#16598) 2024-09-11 18:11:10 +00:00
Syed Ali Shahbaz 89250422ea fix: API V1 slots add explicit orgSlug null fallback (#16593)
* add explicit orgSlug null fallback

* do it in schema

* to nullish
2024-09-11 16:14:03 +00:00
Keith Williams 2f2f4dba60 chore: v4.4.8 (#16597)
release.yml / release (push) Canceled after 0s
2024-09-12 00:19:00 +09:00
Calcom BotandReplexica b292cafe7b feat: update translations (#16592)
Co-authored-by: Replexica <support@replexica.com>
2024-09-11 14:56:28 +00:00
Keith Williams 82053e0c7c perf: Include no shows in main status query (#16594)
* perf: Include no shows in main status query

* Removed the Promise.all call
2024-09-11 14:16:46 +00:00
Udit Takkar 860d173f0a feat: instant meeting browser notifcations (#16480)
* feat: instant meeting browser notifcations

* chore: support multiple notifications

* chore: save progress

* fix: add test notification and move file

* fix: type err

* chore: feedback improvements

* chore: undo DENY
2024-09-11 18:02:14 +04:00
Udit TakkarandSyed Ali Shahbaz 5bc0e1265d fix: use meeting id to get transcripts (#16320)
* fix: use meeting id to get transcripts

* chore

---------

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2024-09-11 22:05:56 +09:00
Hariom Balhara e362c37ff3 fix: Location Change to Organizer Default Conferencing App (#16379)
* Fix

* Dont include test files

* Fix location change

* Self review fixes

* Add unit test

* Add more test

* Add a bookingScenario as well
2024-09-11 21:31:53 +09:00
4d53f327d9 feat: V2 API endpoint create phone call (#16528)
* refactor: V2 API endpoint create phone call

* fix: type err

* fix: type and build err

* chore: change default value

* chore: move it to another route

* test: for create phone call

* chore: undo constant

* chore: remove test

* fix: make begin_message optional

* chore: improvements

* chore: begin message

* chore: remove unused import

* chore: bump platform libraries with handleCreatePhoneCall

* chore: improvement

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-09-11 11:11:54 +00:00
61f436be2a fix: wrong reschedule link for an org member for a booking on event outside the org (#15260)
* fix: wrong reschedule link

* test

* fix

* test

* test

* refactor: get org id of booking

* fix: redirect URL to handle movedToProfile username based on domain context

* Revert not needed changes

* Revert unnecessary changes now

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-09-11 04:47:52 +00:00
Benny JooandUdit Takkar d1a0fc6604 chore: App router migration - Fix /event-types/[type] page, move getServerSideProps to /lib for event-types pages (#16457)
* event-types: refactor and extract client code into /modules

* revert

* remove res in getServerSideProps

* remove res in getServerSideProps 2

* Create EventTypeAppDir and fix error in event-types/[type]

* add comments

* fix type-check

* fix 1 more type-check

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-09-11 02:41:46 +00:00
Benny JooandAnik Dhabal Babu 976013512c perf: Server-Side Data Fetching in App Router: /workflows (#16563)
* wip

* complete server side fetching for app router

* fix type error

* fix

* address comments

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-09-10 19:25:59 -07:00
Benny Joo d9777a3011 perf: Server-Side Data Fetching in App Router: /availability (#16536)
* wip

* add method to OrganizationRepository

* server side fetching for availability

* fix error

* server side fetching finish

* remove log

* fix

* fix

* fix type

* no need to export

* fix type

* address nit comments
2024-09-10 19:23:42 -07:00
Benny JooandUdit Takkar 7725adedc8 chore: App router migration (org/[orgSlug]/instant-meeting), add missing page (#16448)
* Create event repository

* Use repository in event.handler

* fix type check

* migrate org/instant-meeting

* add export default

* fix metadata

* move getServerSideProps from @server/lib to @lib

* fix

* fix

* revert unneeded changes

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-09-10 18:43:50 -07:00
Anik Dhabal Babu 7269b3ea60 revert: booking rejection reason from booking success page (#16584) 2024-09-11 03:51:36 +05:30
Anik Dhabal Babu c168df4df0 fix: booking response field's label is not translated (#16582) 2024-09-10 23:39:32 +02:00
Keith Williams be9b0fb9a8 perf: Insights - put teamId filter first (#16581) 2024-09-10 19:40:19 +00:00
VijayandAnik Dhabal Babu c056cabbbd fix: to allow paypal install on team when user already installed (#16553)
* fix to allow paypal install on team when user already installed

* handle if parseInt fails

* Update add.ts

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-09-11 00:06:10 +09:00
97630e8add fix: prevent creating multiple platform orgs (#16564)
* fix: prevent creating multiple platform orgs

* fixup! fix: prevent creating multiple platform orgs

* fixup! fixup! fix: prevent creating multiple platform orgs

* Bust cache in CI

* Update schema.prisma

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-09-11 00:05:22 +09:00
VijayandAnik Dhabal Babu a5443ccfbe fix to show updated values in eventTypes list (#16569)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-09-11 00:05:02 +09:00
Alex van Andel 6f2986e42e chore: Release v4.4.7 (#16574)
release.yml / release (push) Canceled after 0s
2024-09-10 16:00:47 +01:00
Alex van Andel db345de99e fix: Add missing URL encoding to scheduledBy (#16573) 2024-09-10 13:49:23 +00:00
Keith WilliamsandAnik Dhabal Babu bde40dceb3 chore: Remove separate OR queries for Insights (#16568)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-09-10 13:31:36 +00:00
Hariom BalharaandAnik Dhabal Babu 1a86e7c506 Skip COEP tests (#16572)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-09-10 13:12:59 +00:00
Benny JooandKeith Williams 9ed1624111 chore: App router migration - settings/platform/**/*, settings/organizations/new and settings/organizations/[id]/* (#16334)
* remove WizardLayoutAppDir

* refactor organizations/new and add platform/new

* refactor organizations/[id]/*

* add APP_ROUTER_SETTINGS_ORG_ENABLED

* fix type error

* fix organizations/new page

* remove code not needed and fix type errors

* Fix Metadata utils

* remove unnecessary code

* remove unneeded code

* wip

* do not remove buildWrappedOnboardTeamMembersPage

* fix type error

* do not introduce changes to yarn.lock

* fix wrong route name

* fix onboard members

* fix platform pages

* fix settings/platform main view

* add requiresLicense to WithLayout

* refactor

* add requiresLicense to platform/new

* Add LicenseRequired back to platform/new

* add env variable

* fix onboard-members

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-09-09 17:19:44 -07:00
Benny Joo 5dff1c6df0 chore: App router migration - /attributes, /privacy, /dsync, /license-key under /settings route group (#16406)
* migrate license-key/new

* migrate settings/org/dsync

* migrate settings/org/privacy

* migrate org attributes

* remove res in getServerSideProps

* Fix SettingsLayoutAppDir

* fix attributes pages

* fix

* fix
2024-09-09 17:19:12 -07:00
Benny JooandJoe Au-Yeung 706d7dff42 perf: Server-Side Data Fetching in App Router: SettingsLayout / AdminLayout (#16537)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-09-09 14:38:51 -07:00
Keith Williams 71443ef23d perf: use isTeamBooking filter in all of Insights (#16561) 2024-09-09 18:50:20 +00:00
Keith Williams 17201334b6 Modified the view to use UNION for user/team bookings (#16558) 2024-09-10 03:05:38 +09:00
Peer Richelsen bc82ff4d21 Update and rename bug_report.yml to bug_report.md (#16556)
* Update and rename bug_report.yml to bug_report.md

* Update bug_report.md

* Update bug_report.md
2024-09-09 17:13:02 +00:00
Benny Joo 71ae3f157a chore: Fix bundle error occurring in Next.js Bundle Analysis CI job (#16548)
* fix bundle error

* revert
2024-09-09 22:25:25 +09:00
Rajiv SahalandKeith Williams 8784be067f chore: fix input DTOs for v2 calendar busy times endpoint (#16526)
* add class-transformer package to type

* update input dtos for get busy times and fix type issue

* fixup

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-09-09 09:26:16 +00:00
84a2e55125 chore: App router migration - general settings (#16312)
* added lockedSMS to future routes

* add orgMigrations routes to future

* correct metadata

* small fix

* add orgMigrations to future

* Move components to client components

* Remove tRPC element from edit user RSC

* Get username for meta

* Remove suspense query

* Remove orgMigrations from app router

* Type fix

* Revert "Remove suspense query"

This reverts commit eadd814f6e4a5d6856d9218342b7909c22fe62c6.

* Handle suspenseQuery in app router

* User edit page, fetch data server side

* Update yarn.lock

* Export getFixedT

* Set PageWrapper as root layout for settings

* Settings Layout accepts strings for shell heading

* Add OOO to app router settings

* Refactor layout for my-account pages

* Remove instances of pages router from my-account

* Refactor security pages

* Add billing to app router

* Add admin API link to layout

* Add api keys page

* Webhooks WIP

* Refactor SettingsHeader to client component

* Add webhook pages

* Refactor API keys page

* Add admin app page

* Type fix

* fix types

* fix developer/webhooks/[id] param value type error

* remove unnecessary code

* do not pass t prop to CreateNewWebhookButton

* fix type errors in webhook-edit-view

* fix the remaining type errors

* do not use prisma directly in generateMetadata

* remove use client if unnecessary

* Remove unused shell heading from SettingsLayoutAppDir

* improve metadata

* fix billing page

* fix import in settings/teams

* Use next `notFound()`

* fix type check

* fix type check

* remove unused code

* Fix calendar setting page

* Separate settings pages into route groups

* Refactor admin settings pages

* Remove meta instance from billing page route

* Update settings layoutAppDir

* Refactor developer settings pages

* Refactor out of office

* Refactor my account settings

* Refactor admin api page

* Refactor security pages

* Type fix

* fix styling in settings layout

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2024-09-09 01:15:03 -04:00
Calcom BotandReplexica 21e23f32ac feat: update translations (#16532)
Co-authored-by: Replexica <support@replexica.com>
2024-09-07 19:30:52 -03:00
Anik Dhabal Babu 33aa9420af fix: fixed the flaky e2e tests that failed the most times and refactor (#16364)
* fix: fixed the flaky tests that failed maximum times

* Update action-based.e2e.ts

* another

* add

* add another

* update

* fix

* add

* fix

* update

* update and add

* fix

* update

* update

* chnages

* chore: updates

* reafactor and following best practice

* fix

* add and update

* update

* update and fix

* fix type

* another test

* update

* Update organization-invitation.e2e.ts

* refactor and update

* fix

* final update
2024-09-07 18:52:55 -03:00
Hariom Balhara ef0ba48f49 fix: Invalid options for Select and MultiSelect when both legacy and new options(i.e. with and without id are present) (#16509)
* Fix invalid select options

* Add test for multiselect filling and fix bug with csv reporting

* Fix options getting duplicated

* Add getQueryBuilderConfig tests
2024-09-07 03:07:47 +00:00
Emilio Degiovanni 6e54c459a4 fix: #16083. booking location editable (#16142) 2024-09-06 20:01:35 +00:00
f5c16f0d6b feat: OOO webhook and zapier (#15434)
* zapier and webhook

* instructions for zapier

* Fix types

* fix types

* revert adding zap template

* Update packages/app-store/zapier/README.md

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* Update packages/app-store/zapier/README.md

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* Update packages/app-store/zapier/README.md

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* Update packages/features/webhooks/lib/scheduleTrigger.ts

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* wip

* Fixes from comments, no more sendPayloadNoBooking

* fix subscriberOptions teamIds param type

* types

* Update packages/features/webhooks/lib/sendPayload.ts

* Re add zapierPayload to don't break old zaps

* instead of metadata use oooEntry inside webhookPayload

* undo comment message

* Types

* reset changes on yarn.lock

* review changes

* fix types

* fix types

* tentative fix for types in webhook

* type improvements

* fix description + clean up

* revert yarn.lock changes

* allow custom template for ooo entry

* type fixes

* type fix

* fix donwloadLinks in payload

* simplify some types

* allow array or number for teamId

* same payload for test trigger

* code clean up

* fix no show e2e test

* translate text for webhook payload and fix conditional

* add test for ooo_created webhooks

* reset files

* merge fix

* fix test data test id

* remove unused variable

* only trigger for accepted memberships

* remove unused variable

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@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: Omar López <zomars@me.com>
2024-09-06 15:48:05 -04:00
Morgan 105e7d3e28 chore: booker atom username / teams props enabled queries (#16514) 2024-09-06 12:38:37 -07:00
Morgan 21ef1900e9 chore: get platform me from trpc instead of apiv2 (#16515) 2024-09-06 12:36:47 -07:00
Peer Richelsen f07b73dc79 chore: updated cookie usage i18n (#16530) 2024-09-06 18:36:02 +00:00
Benny Joo d4b0516235 chore: App router migration - (/more, /maintenance, /connect-and-join, /upgrade) finish migration (#16497)
* more: extract client component into /modules folder

* maintenance: extract client component into /modules folder

* upgrade: extract client component into /modules folder

* improve WithLayout wrapper

* connect-and-join: extract client component into /modules folder

* add env vars
2024-09-06 17:22:05 +00:00
Calcom BotandReplexica 7bfad11ff9 feat: update translations (#16524)
Co-authored-by: Replexica <support@replexica.com>
2024-09-07 02:01:17 +09:00
d2f7a427d0 chore: App-router-migration(/apps) fix metadata, extract components into /module and finish migration (#16354)
* fix metadata for apps page

* migrate apps route to modules

* migrate apps/[slug]

* migrate apps/[slug]/setup

* migrate apps/categories/**/*

* add apps/installed page to app router

* migrate apps/[slug]/[...pages]

* migrate apps/installed/**/*

* Add apps/installation/[[...step]] to App router

* fix imports

* add missing use client directive

* finish migration

* fix metadata

* fix metadata for apps/categories

* fix type for installation/[[...step]]

* fix type for apps/[slug]/[...pages]

* remove unnecessary code

* refactor

* fix apps/installation

* move getServerSideProps to lib

* fix import of apps/installation

* fix import for app slug page

* refactor

* fix installation page in app router

* remove res in getServerSideProps

* replace setHeader with NextResponse header setHeader

* refactor installed pages

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-09-06 15:40:19 +00:00
Morgan a910eb96ad chore: improve typing of runSeparateQueriesForOrStatements (#16525)
* chore: improve typing of runSeparateQueriesForOrStatements

* fixup! chore: improve typing of runSeparateQueriesForOrStatements

* fixup! fixup! chore: improve typing of runSeparateQueriesForOrStatements

* fixup! fixup! fixup! chore: improve typing of runSeparateQueriesForOrStatements
2024-09-06 14:50:33 +00:00
Keith Williams c080458faa fix: Broken preview deployments (#16523) 2024-09-06 12:58:41 +00:00
Keith Williams 5c75cfc17f chore: make generic function for separate or queries (#16522)
* trying to create generic function for ors

* chore: Add generic function to run separate queries for OR

* Refactored getTotalNoShows to use separate queries

* Finished noshows

* Made the no shows logic handle single result
2024-09-06 12:27:05 +00:00
Keith Williams 4280f556cb chore: Add last batch of languages to translate with AI (#16520) 2024-09-06 12:26:22 +00:00
Syed Ali Shahbaz 21368bb130 Update FeedbackTable.tsx (#16521) 2024-09-06 11:21:07 +00:00
Benny Joo e9076c8e3e Refactor AppProvidersAppDir (#16496) 2024-09-06 08:53:25 +00:00
2471ee801b feat: hashed link retrieval from event types v1 api (#16437)
* add hashed link retrieval from event types api v1

* remove id, eventTypeId from hashedLink field

* Update event-type.ts

* Update event-type.ts

---------

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: Peer Richelsen <peeroke@gmail.com>
2024-09-06 06:47:23 +00:00
+6 9fc9876b3f refactor: allow managed event tests parallel runs (#16113)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Replexica <support@replexica.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
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>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-09-06 05:32:16 +00:00
Omar López 34ca3bd22d fix: flakyness on booking questions tests (#16513) 2024-09-06 07:20:25 +05:30
Anik Dhabal Babu bebe1dd7b4 chore: improved lastused login method (#16511) 2024-09-05 23:19:24 +05:30
fced9a6355 feat: Add step in team creation form for selecting event type and create a team event (#15286)
* feat: Create team event on a new team creation

* fix

* fixed failed tests

* move team event type form

* fix type error

* requested chnages

* update

* some update and refactor

* fix e2e test

* Update useCreateEventType.ts

* update

* fix type error

* fix e2e

* update

---------

Co-authored-by: Somay Chauhan <somaychauhan98@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-09-05 12:33:18 -04:00
Keith Williams 6b57dd6794 chore: v4.4.6 (#16507)
release.yml / release (push) Canceled after 0s
2024-09-05 16:21:34 +00:00
Calcom BotandReplexica eb4c58186b feat: update translations (#16508)
Co-authored-by: Replexica <support@replexica.com>
2024-09-05 16:13:40 +00:00
Keith Williams 17e438ab9f chore: Add next batch of languages to AI translations (#16506) 2024-09-06 00:07:26 +09:00
Keith Williams 458e6a3abf perf: Separate OR clauses from Insights counts (#16502)
* perf: Separate OR clauses from Insights counts

* fixed bug with delete statement

* Fixed types

* Reverted unneeded change

* Need to use += so we capture data from all queries
2024-09-05 23:01:54 +09:00
Benny Joo 963d22648b chore: App-router-migration(/availability) add pages / extract components into /module (#16404)
* migrate availability/[schedule]

* add use client directive

* migrate main availability page

* migrate availability/troubleshoot page

* add env var

* extract AvailabilitySettingsWebWrapper into /modules

* fix build

* add missing default

* add missing export

* fix troubleshoot

* use params, not searchParams in generateMetadata

* fix schedule page in app router
2024-09-05 12:37:18 +00:00
fabcf2055b feat: Ability to Hyperlink text in Checkbox on Additional Questions (#15194)
* feat: Ability to Hyperlink text in Checkbox on Additional Questions

* update

* fix and update

* fix type error

* fix and update

* dpdate

* fix type error

* fix unit test

* Update

* fix tests

* revert test

* remove yarn.lock

* remove schema.prisma

* update

* remove log

* revert

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-09-05 09:05:45 +00:00
Lauris Skraucis 633697a812 fix: v2 api fix recurring booking (#16472)
* fix: v2 POST recurring booking

* refactor: have default recurringEventId for recurring booking
2024-09-05 08:37:26 +00:00
sean-brydonandMorgan Vernay 2de33fe1bf feat: attributes v2 api (#16461)
* WIP

* services

* controllers + outputs

* unit tests for repositories

* use hard coded enum type

* use prisma import

* remove redunant input file

* fix packagejson

* rename to spec files

* fix typo in option service file name

* move membership call to org membership service

* move logic to service > repo

* fix membership query

* attribute controller e2e spec

* add options e2e test

* add jest watch for e2e

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-09-05 10:15:12 +02:00
Omar López 36e79bf69c ci: adds basic visual regression testing for icons (#16408) 2024-09-04 16:28:42 -04:00
Joe Au-Yeung 2e36cadce4 fix: Add jitter when Outlook requests are throttled (#16484) 2024-09-04 13:25:31 -07:00
Syed Ali Shahbaz 9ef532019f fix: v1 API Teams access (#16403) 2024-09-04 19:13:38 +00:00
Benny Joo 176dd1a7f3 edit deploy script to enable future/auth (#16492) 2024-09-04 19:09:57 +00:00
e9c71abd6f fix: meeting url missing in workflow email (#16434)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-09-04 17:56:20 +00:00
Imamuzzaki Abu Salam eb472cba36 chore: Remove obsolete docker compose version (#16419) 2024-09-04 13:02:30 -03:00
Benny Joo 7d77554696 chore: App router migration - Add env variables for auth/forgot-password/*, auth/login, auth/logout, auth/new, `auth/saml-idp (#16488)
* Add env variables for auth/forgot-password, auth/login, auth/logout

* Add env variables for auth/new, auth/saml-idp
2024-09-04 15:20:40 +00:00
Morgan 31bfa9e32e fix: api v2 booking controller handle error codes (#16486)
* fix: api v2 booking controller handle error codes

* fixup! fix: api v2 booking controller handle error codes

* fixup! Merge branch 'main' into fix-booking-controller-errors-api-v2
2024-09-04 10:56:00 -03:00
Benny Joo 26be88fc56 chore: App-router-migration /auth/error, /auth/platform, /auth/oauth2 (#16445)
* preparePageMetadata: improve logic for title

* add missing pages: oauth2 / platform

* auth/error: add to app router and extract to /modules
2024-09-04 13:15:19 +00:00
Benny Joo 86cf8feb55 chore: App-router-migration /auth/new and /auth/saml-idp (#16447)
* auth/new: add to app router

* auth/saml-idp: add to app router

* fix
2024-09-04 13:05:38 +00:00
Lauris Skraucis 1edd255846 fix: use dbWrite in OrganizationsTeamsRepository (#16485) 2024-09-04 11:17:13 +00:00
Lauris Skraucis f538877349 chore: v2 bump platform libraries (#16473) 2024-09-04 11:14:05 +00:00
Keith Williams d759669c1e chore: Remove unnecessary log (#16426) 2024-09-04 11:09:10 +00:00
Peer Richelsen 71ce87ca21 Update bug_report.yml (#16089) 2024-09-04 11:05:43 +00:00
Calcom BotandReplexica 7eae004aa2 feat: update translations (#16477)
Co-authored-by: Replexica <support@replexica.com>
2024-09-04 10:39:16 +00:00
Udit TakkarandMorgan 6cd427bc78 feat: availability in instant meeting (#16424)
* chore: save progress

* feat: add isAvailable functionality

* fix: type error

* chore: add in builder

* tests: add unit tests

* chore: improvements

* chore: tests

* chore

* chore: remove log

* fix: tets

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-09-03 14:28:15 +00:00
Darshil MharaurandAmit Sharma b8d67c5a41 fix: Adding yearly title to preview page (#16441)
* initial commit

* added year tag to the occurences field

* Revert yarn.lock

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-09-03 09:34:05 -04:00
Calcom BotandReplexica 42953548b6 feat: update translations (#16475)
Co-authored-by: Replexica <support@replexica.com>
2024-09-03 13:20:21 +00:00
Hariom Balhara 28c631e108 test: Unit Tests for FormBuilderField and BookingFields components (#16162)
* Remove use of location from FormBuilder

* Add tests

* FormBuilderField and BookingFields tests

* More tests

* Remove always true if condition

* Fix ui import mockig that got broken after the last merge
2024-09-03 08:58:34 -04:00
Keith Williams 263a96e660 chore: Add 3 more languages to AI translations (#16469) 2024-09-03 13:34:50 +01:00
Benny Joo d059b1399b chore: App router migration - /auth/forgot-password/*, /auth/login, /auth/logout (#16467)
* migrate auth/forgot-password/*, auth/login, auth/logout

* revert

* fix forgot password

* fix folder structure

* fix logout

* fix middleware

* remove TODO log

* refactor logout
2024-09-03 18:53:16 +09:00
Benny Joo 12a9c74eb6 chore: App router migration - Add method to BookingRepository & Extract /videos/**/* into /modules and /lib (#16466)
* meeting-ended: refactor and extract client code into /modules

* meeting-not-started: refactor and extract client code into /modules

* meeting-not-found: fix getData

* BookingRepository: add a new method

* video: refactor and extract client code into /modules

* refactor

* refactor getServerSideProps

* use findBookingByUid for more getServerSideProps functions

* rename findBookingByUid -> findBookingByUidWithOptionalSelect

* add safety
2024-09-03 18:53:02 +09:00
Keith Williams 6bb60e36fd chore: Log cold starts of /v1/slots (#16468) 2024-09-03 06:54:12 +00:00
Calcom BotandReplexica c1eb2512b6 feat: update translations (#16462)
Co-authored-by: Replexica <support@replexica.com>
2024-09-03 15:43:44 +09:00
Benny Joo 55d75a9786 chore: App router migration (/workflows) fix metadata and remove duplicate file for getStaticProps (#16452)
* Fix metadata

* add use client directive to workflow component
2024-09-02 23:00:48 +05:30
779eb19e7a feat: browser push notifications (#14888)
* feat: web push notifications feature

* Revert yarn.lock changes

* added new env variables requirement in .env.example

* moved useNotifications hook in packages/lib/hooks

* fix: bug

* use i18n

* chore: move to new file

* chore: add yarn.lock

* Update .env.example

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

* chore: add instruction for brave browser

* fix: tooltip

* chore: use enum

* chore

* small update

---------

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>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-09-02 14:39:51 +00:00
Hariom BalharaandUdit Takkar 5d8ed130de Fix unconfirmed bookings blocking slots being fetched for all troubleshooters (#16439)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-09-02 16:12:11 +05:30
Calcom BotandReplexica 971d9ff0d8 feat: update translations (#16436)
Co-authored-by: Replexica <support@replexica.com>
2024-08-31 15:17:34 +00:00
Anik Dhabal Babu b6bcc43655 feat: add "somewhere else" as a location option (#15108)
* feat: add somewhere else as a location

* update

* update

* Update BookingFields.tsx

* update

* update

* chore: changes

* fix
2024-08-30 16:38:09 -04:00
Peer Richelsen b576fc8c26 feat: synthflow app (#16420) 2024-08-30 16:38:01 +00:00
Kacper 821843bc6c Fallback to next app default language (en) when user didn't specify locale. (#16425) 2024-08-30 21:37:10 +05:30
Lauris Skraucis d19e3e53f0 fix: recurring booking idempotency key after cancel (#16414)
* fix: recurring booking idempotency key after cancel

* refactor: move logic to updateMany of prisma middleware
2024-08-30 12:07:53 +00:00
Morgan 40fee7af51 chore: reorganize event type files (#16393)
* chore: reorganize event type files

* fixup! chore: reorganize event type files

* fixup! Merge branch 'main' into refactor-event-types-components-folder
2024-08-30 10:41:37 +00:00
Keith Williams f3462776e8 perf: Improve loading existing bookings via API (#16410) 2024-08-30 11:10:44 +01:00
Calcom BotandReplexica 57e9ef7320 feat: update translations (#16401)
Co-authored-by: Replexica <support@replexica.com>
2024-08-29 23:26:01 +00:00
88f46a6b20 fix: Broken reschedule page for migrated user's booking through request-reschedule (#16068)
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2024-08-29 14:35:32 -07:00
Omar López 17f537774c chore: v4.4.5 (#16402)
release.yml / release (push) Canceled after 0s
2024-08-29 14:26:05 -07:00
3aa6878c27 fix: Set teamMemberEmail server side for CRM RR Skip (#16367)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2024-08-29 14:16:48 -07:00
Benny JooandJoe Au-Yeung ae55e5e264 chore: App Router Migration - Ensure isBookingPage is correctly flagged before being passed to PageWrapper (#16362)
* Ensure adding isBookingPage is done in components in /modules, not in /pages

* Detect isBookingPage prop coming from Page and add it to page wrapper app dir

* fix build

* fix

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-08-29 17:03:02 -04:00
Hariom Balhara 36ecece181 fix: Add version to embed.js and preview.js (#16182) 2024-08-29 20:54:41 +00:00
Hariom Balhara 1a0448910b fix: Flicker b/w Dark and Light theme (#16042) 2024-08-29 13:14:12 -07:00
ad5d645347 feat: Implement editable OOO events feature (#15932)
* feat: Implement edit ooo feature

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

* refactor: Refactor button text and gap between buttons

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

* fix: Fix entry create and edit edge cases

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

* chore: Change "edit ooo event" modal title

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

* chore: Add tooltips to entry eidt and delete buttons

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

* feat: Reset form on form close event

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

* refactor: Refactor reset-form function

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

* chore: Rename create or edit OOO as suggested

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

* refactor: Refactor component and function names

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

* feat: Improve ooo events email notifications

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

* fix: Fix email templates

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

* feat: Add e2e test for ooo event edit functionality

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

* refactor: Create separate test for edit ooo event

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

* fix: Remove extra user from redirect user configuration test

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

* chore: code refactor

* remove log

* chore: Update ooo-input-schema uuid data type

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

* fix: Address feedbacks

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

* chore: code splitting and refactor

---------

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-08-29 17:28:35 +00:00
Calcom BotandReplexica 54f7c8328d feat: update translations (#16400)
Co-authored-by: Replexica <support@replexica.com>
2024-08-29 17:13:06 +00:00
Keith Williams 36a1715ece perf: run parallel bookings queries (#16398) 2024-08-29 16:37:18 +00:00
Keith Williams edaa6b1de8 feat: Add more AI translation languages (#16381)
* feat: Add more AI translation languages

* Removed pull-request: true

* checking out the PRs i18n.json as well

* Update i18n.json

* Update i18n.json
2024-08-29 17:18:48 +01:00
Calcom BotandReplexica f47b2066c4 feat: update translations (#16396)
Co-authored-by: Replexica <support@replexica.com>
2024-08-29 15:58:25 +00:00
Keith Williams 7337b3de1e fix: GitHub access token for Replexica (#16395) 2024-08-29 15:02:05 +00:00
Keith Williams 933ffcbf69 chore: Move Replexica to run on pushes to main (#16394) 2024-08-29 14:22:42 +00:00
Syed Ali ShahbazandMorgan cf50566a10 feat: Org Webhooks API V2 (#16274)
* --init

* add is webhook in org guard

* --

* doc

* e2e

* --feedback

* --

* fix typo

* delete unnecessary file

* fix v2 e2e

* fix v2 e2e

* fix e2e

* fix v2 e2e

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-08-29 17:52:42 +04:00
Sy SagarandAmit Sharma 4424aa5b38 chore: Bulk update desc (#16343)
* chore(locale-en): update bulk modal description

* chore: schedule bulk-modal description

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-08-29 08:16:13 -04:00
Udit TakkarandJoe Au-Yeung 19634073e9 refactor: improve team members page performance (#16155)
* fix: simplify workflow page and improve load time

* chore: use new endpoint

* chore: save progress

* refactor: code

* refactor: remove not requried code

* chore: remove schema

* chore: fix typ

* chore: improve

* chore: change name

* chore: remove unused

* chore: remove page

* refactor: teams page

* feat: add auto scroll

* chore: create validate unique invite

* fix: auth check

* fix: optimistic update

* chore

* fix: add loading

* fix: improvements

* chore: remove

* chore

* chore: fix teams page

* fix: team profile page

* fix: appearance page

* fix: sso view

* fix: type err

* feat: defer loading connected Apps

* fix: type err

* fix: type error

* fix: type err

* fix: connectedApps type

* chore: move

* chore: missing export

* feat: add search by name

* fix: display role change

* fix: use setInfiniteData

* chore: save progress

* test: add unit tests for loading members

* fix: test

* chore: update name

* fix: bugs and improvements

* chore: change variable name

* test: add tests for checkCanAccessMembers

* refactor: performance

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-08-29 13:32:54 +05:30
Keith Williams 4284e86e51 perf: Removed unused attendees count (#16391) 2024-08-29 01:13:48 -04:00
Max Prilutskiy f7c1d62bb8 chore: [skip i18n] switch pr's i18n job to in-branch mode (#16390) 2024-08-29 01:32:03 +00:00
795a228a44 fix: allow all sign in methods when being invited (#13436)
* allow all sign in methods

* changes

* update

* add checks

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-08-28 15:24:13 +00:00
Nicolaos MoscholiosandAnik Dhabal Babu 0190ca6711 fix: use userPhone instead of phone for switch case (#16384)
* fix: use `userPhone` instead of `phone` for switch case

Currently this switch case never gets to the last case ("phone") as the type expected is wrong.

Here the type may have value `userPhone` and not `phone`.

* fix another case

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-08-28 13:37:02 +00:00
Morgan ee8cc81862 fix: api v2 booking controller with api key (#16358)
* fix: api v2 booking controller with api key

* add test

* fixup! add test

* fixup! Merge branch 'fix-booking-controller-api-key' of github.com:calcom/cal.com into fix-booking-controller-api-key

* fixup! Merge branch 'fix-booking-controller-api-key' of github.com:calcom/cal.com into fix-booking-controller-api-key
2024-08-28 16:05:14 +03:00
Joe Au-Yeung 6b244fc5b5 fix: Move future/workflow/[workflow] getStaticProps into separate file (#16124) 2024-08-28 08:53:32 -04:00
Udit Takkarandsean-brydon 356ecfe2fa chore: display first 3 organizers photo in round robin event (#16331)
* chore: display random organizer photo in round robin event

* fix: avatar on instant booking

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-08-28 12:31:06 +00:00
Keith Williams bd410cf09f chore: Use pull-request feature from Replexica (#16383) 2024-08-28 19:24:56 +09:00
Keith WilliamsandReplexica 3c5ab5e8d2 chore: add replexica to ci (#16306)
* chore: Add replexica for AI i18n

* Add write permission

* moved i18n to repo root

* Added checkout to i18n job

* Added contents:read

* Added actions: write for testing

* Updated contents: write

* feat: add missing translations

* Checking out the i18n files from PR head sha

* Added the rest of the languages

* Added i18n to list of required checks

* Added a comment explaining the custom common.json checkout

* Commented out languages to not crush Replexica usage

* Add a fetch-depth: 2

* Supporting fetch of external fork branch

* Put back the dynamic sha

* Added a check for null so this can run manually and not just on PRs

* Removed comments

* feat: update translations

* Reverted de and fr to re-run with new settings

* feat: update translations

* Reverted de and fr to re-run with new settings

* feat: update translations

* Removed crowdin

* Limited languages to major ones for 1st pass

* feat: update translations

* Fixing es test

* Reverted yarn.lock changes

---------

Co-authored-by: Replexica <support@replexica.com>
2024-08-28 11:16:47 +03:00
Udit Takkar d938e67e65 fix: require confirmation cal video bug (#16376)
* fix: require confirmation cal video bug

* chore: update location

* fix: tests

* chore

* test: add confirm booking test

* chore: add location in test

* fix: types
2024-08-27 22:40:00 +00:00
sean-brydon 6a90518231 use goblin url (#16370) 2024-08-27 22:30:18 +00:00
Benny Joo 47e8907184 chore: App Router Migration - Fix metadata util used in App Router and remove duplicate code (#16346) 2024-08-27 13:28:09 -04:00
Keith Williams d6096f459b chore: v4.4.4 (#16372)
release.yml / release (push) Canceled after 0s
2024-08-27 22:38:34 +09:00
Keith Williams 8839f08257 fix: API v2 E2E Docker rate limiting (#16374) 2024-08-27 12:25:53 +00:00
Hariom Balhara 6104add6bd fix: More Stability for FormBuilder by unit tests (#16129)
* Remove use of location from FormBuilder

* Add tests

* Improve error message in test

* Bust cache
2024-08-27 08:14:19 -04:00
Johannes ZellnerandAnik Dhabal Babu 7aae55f417 fix: Avoid typescript error for Nextjs usePathname() (#16368)
* Avoid typescript error for Nextjs usePathname()

#fixes 16323

* Update Shell.tsx

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-08-27 07:46:55 +00:00
Keith Williams ef1b5067c2 fix: Undefined user in insights data (#16365)
* fix: Null user in insights data

* renaming
2024-08-26 17:43:04 -07:00
Anik Dhabal Babu 4caa095d52 fix: color restrictions issue (#16360)
* fix: color restrictions issue

* Update EventAdvancedTab.tsx
2024-08-27 07:59:09 +09:00
sean-brydonandAlex van Andel c8e20b0c57 feat: block pending meetings for requires confirmation (#16335)
* feat: block pending meetings for requires confirmation

* add i18n

* fix:typecheck

* fix(requires-confirmation-block): Fixes type error

* fix(requires-confirmation-block): Fixes type error

* Tweak to set BookingWhereInput as the explicit type instead of as

* fix: type error

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-08-26 09:54:58 -04:00
Anik Dhabal Babu 2fb4289798 feat: tracking and displaying last used login method (#16356) 2024-08-26 12:13:42 +00:00
6529b49c3c feat: bookingFields identifier supports - and _ characters (#16353)
* now bookingFields identifier supports - and _ characters but no special characters

* fix: unit test

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-08-26 12:15:02 +02:00
758423ecf9 fix: Text for cookie consent checkbox in various languages (#16355)
* Fix text for cookie consent checkbox in various languages

* update

---------

Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-08-26 09:03:20 +00:00
Somay ChauhanandMorgan Vernay ef11755488 feat: add recurringEvent attribute to api/v2/event-types (#16251)
* feat: added recurringEvent attribute to api/v2/event-types

* changed `recurringEvent` to `recurrence`

* Update CHANGELOG.md

* remove /dist/ import

* change `as` to `satisfies`

* testing some changes

* Revert "testing some changes"

This reverts commit 464d2611ed19dbd6ae390332b4d6dd5884bcae3b.

* Update recurrence.input.ts

* fix: apiv2 tsconfig enums

* Update CHANGELOG.md

* removed dev imports

* Update package.json

* Update api-request.spec.ts

* Update api-request.spec.ts

* added recurrence to update-event-type.input

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-08-26 11:42:35 +03:00
Steve Xu 03051a2bb1 fix: fix some missing translation (#16351) 2024-08-26 10:38:23 +02:00
CodeShakingSheep 7a57f57e99 chore: Make URLs for terms and privacy policy customizable via .env vars (#16350) 2024-08-26 10:34:07 +02:00
b6d31129a3 feat: Add rescheduledBy & canceledBy fields in the DB (#15337)
* feat: Add rescheduledBy & canceledBy fields in the DB

* fix: type check

* fix: type check

* fix: use session user email for reschedule

* fix: unit test

* feat: db field email validation

* feat: rescheduledBy and cancelledBy in webhooks

* revert unrelated changes.

* make session user secondary, default to Anonymous

* if condition not required

* Make cancelledBy optional

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>

* update cancel booking type

* fix: update cancelledBy in db when requesting reschedule

* remove default value for fields

* fix: type check

* feat: manage fields via api v1

* fix: add fields in booking read api v1

* test: expand to cover new fields

* fix: use cancelledBy param on booking page

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-08-25 11:31:46 +02:00
Steve XuandAnik Dhabal Babu 34075ee303 fix: multiple_duration_timeUnit_short Chinese translation (#16339)
* fix: multiple_duration_timeUnit_short Chinese translation

* fix: i18n

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-08-25 09:30:33 +00:00
Steve Xu 89f711d929 fix: fixing i18n key signing_up_terms is lost (#16338)
* fix: fixing i18n key signing_up_terms is lost

* fix: fixing i18n key signing_up_terms is lost
2024-08-25 10:52:30 +02:00
623f0c7d93 fix: visiting organization without access shows empty page (#15103)
* fix : visiting organization without access shows empty page

* feat: add serversideprops for navigating user before page load

* Update packages/features/ee/organizations/pages/settings/getServerSidePropsMembers.tsx

* fix: workaround e2e fix

---------

Co-authored-by: Shaik-Sirajuddin <sirajudddinshaik30@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-08-24 09:05:50 +00:00
Rajiv SahalandMorgan c67877cc51 chore: rename calendar settings atom (#16329)
* update stripe service

* init endpoints for stripe connect atom

* update stripe controller

* update stripe service

* add generic function to create app credential

* update stripe controller

* update stripe service

* add handler to construct redirectUrl

* restructure files for better readibility

* better naming

* replace STRIPE_PRIVATE_KEY with STRIPE_API_KEY

* fix naming

* remove unused query param

* frontend for stripe connect atom

* custom hooks for stripe connect atom

* abstract response schema into separate file

* fixup

* input dto for stripe controller

* update stripe module

* fixing naming

* fix naming

* rename atom from `SelectedCalendarSettings` to `SelectedCalendarsSettings`

* revert stripe controllers commit

* fixup

* bring back stripe module

* remove stripe atom

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-08-23 10:40:22 +00:00
Alex van Andel fd50728169 Revert "fix: uncaught types errors in invite members team handler (#16109)" (#16322)
This reverts commit a091c60618.
2024-08-23 05:40:30 +00:00
Omar López 74567909b2 fix: Migrates E2E reports to external repo (#16326) 2024-08-22 14:28:54 -07:00
sean-brydonandKeith Williams d84f08c998 actually delete the item if no options or input is found (#16298)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-08-22 23:28:49 +09:00
Keith Williams 685eadc30b chore: v4.4.3 (#16321)
release.yml / release (push) Canceled after 0s
2024-08-22 14:28:17 +00:00
Udit TakkarandHariom Balhara c821f15a14 fix: error message when email exists (#16022)
* fix: error message when email exists

* chore: change postion of error

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2024-08-22 14:20:41 +00:00
Morgan 8a972a74c6 refactor: bulk update modal logic shifted to web wrapper (#16315) 2024-08-22 13:42:43 +00:00
cf76cf7aae feat: Multiple Options Paste and Routing Form improvements related to Select fields (#15706)
* Add handlePaste function and integrate it with options fields

* Prevents function running if user does not paste a list, allows list items to be pasted without deleting following items

* Subs useState for rhf useWatch to fix issues with multiple form fields

* Sets options array as optional property in zod schema

* Assigns default value to options array to fix possibly undefined type errors

* Sets placedholder property as optional

* maps selectText to options on mount

* Removes comma & semicolon delimiting for pasted lists (new line only) & defines regex as a constant

* Sets default value of placeholder to empty string, provides default fallback for component text field if placeholder is omitted.

* Fix type error on placeholder

* Extracts handlePaste to service function & tests thoroughly

* Some improvements and tests arent needed

* More fixes

* Fix reporting as well

* TS fixes

* Do transforms to and fro from label

* Add unit tests

* Unit test reporting

* Test Improvements

* Self review feedback addressed

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-08-22 13:38:46 +00:00
Keith Williams 4dd762681c chore: Update how E2E test suite checks for Stripe (#16319) 2024-08-22 13:19:50 +00:00
Joe Au-YeungandCarina Wollendorfer 399a23a52f fix: Disable ICS file generation when destination calendar is Outlook (#16247)
* Set the organizer in the organizer field

* Create generateIcsFile

* Add role enum and only disable for organizers

* Move emails to generateIcsFile

* Add new ics func to new emails

* Fix tests

* Type fix

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-08-22 09:49:53 +00:00
Udit Takkar 342f935aba fix: create event type refresh bug (#16301)
* fix: create event type refresh bug

* chore

* fix: type err
2024-08-22 09:43:09 +00:00
27e4210693 fix: atoms icons and build issues (#16248)
* fix: atoms icons and build issues

* fixup! fix: atoms icons and build issues

* fixup! fixup! fix: atoms icons and build issues

* fix import paths

* rename SchedulingType to something more specific

* fix: Invalid define value (must be an entity name or valid JSON syntax)

* chore: atoms build CI job

* chore: enable atoms build in CI

* fix build

* fixup! fix build

* fixup! fixup! fix build

* chore: replace calendar-days icon with calendar

* fixup! Merge branch 'fix-atoms-build-icons' of github.com:calcom/cal.com into fix-atoms-build-icons

* fixup! Merge branch 'fix-atoms-build-icons' of github.com:calcom/cal.com into fix-atoms-build-icons

* fixup! Merge branch 'fix-atoms-build-icons' of github.com:calcom/cal.com into fix-atoms-build-icons

---------

Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
Co-authored-by: Ryukemeister <sahalrajiv6900@gmail.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
2024-08-22 08:32:39 +00:00
Amit SharmaandUdit Takkar 66cb2bcc13 fix: Workflow action 'email to host' only sends to one host of team e… (#14767)
* fix: Workflow action 'email to host' only sends to one host of team event

* add in missing files

* fix host logic

* uncomment working test

* code refactor

* use connected calendar email

* add changes to util file

* fix: extend eventType info in sendCancelledReminders

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-08-22 08:04:31 +00:00
Akash RajpurohitandAnik Dhabal Babu 30c31d3585 feat: ✨ support for adding script url added for umami (#16300)
* feat: ✨ support for adding script url added for umami

This will help self hosters in setting up umami.

* feat: ✨ set default value for script URL in the form field

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-08-22 09:39:03 +02:00
Omar López e17303ea88 fix: permissions issue in pr labeler (#16245) 2024-08-22 01:22:07 +01:00
Anik Dhabal BabuandAmit Sharma bebf1d80a7 fix: round robin event rejection problem (#16279)
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-08-21 17:07:30 +00:00
Benny Joo 5bcefb0b3e settings/teams/[id]/sso is not needed (#16288) 2024-08-21 16:47:34 +00:00
Harshit SharmaandAnik Dhabal Babu f52de7331f fix: UI Breaks When Entering a Long Username in Profile Settings (#16304)
* feat: Improve UI for username availability in UsernameTextfield component

* fix: Fixed UI for username availability in UsernameTextfield component

* Update UsernameTextfield.tsx

* fix: FixedUI for username availability in PremiumTextfield and UsernameTextfield components

* fix: Fixed the PremiumTextField.tsx

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-08-21 16:17:14 +00:00
MorganandLauris Skraucis 1690770d6a fix: check profiles and orgId from user.profiles apiv2 (#16249)
* fix: check profiles and orgId from user.profiles apiv2

* fixup! fix: check profiles and orgId from user.profiles apiv2

---------

Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
2024-08-21 15:15:48 +00:00
Rajiv Sahal 7759e6a029 chore: update platform billing plans (#16233)
* update links for no platform plan

* sync platform pricing with one we have in website
2024-08-21 20:14:15 +05:30
1282 changed files with 69463 additions and 21578 deletions
+30 -1
View File
@@ -334,6 +334,8 @@ AB_TEST_BUCKET_PROBABILITY=50
# whether we redirect to the future/event-types from event-types or not
APP_ROUTER_EVENT_TYPES_ENABLED=0
APP_ROUTER_SETTINGS_ADMIN_ENABLED=0
APP_ROUTER_SETTINGS_PLATFORM_ENABLED=0
APP_ROUTER_SETTINGS_ORG_ENABLED=0
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
APP_ROUTER_APPS_SLUG_ENABLED=0
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
@@ -341,13 +343,28 @@ APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
APP_ROUTER_BOOKING_ENABLED=0
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
APP_ROUTER_WORKFLOWS_ENABLED=0
APP_ROUTER_SETTINGS_TEAMS_ENABLED=0
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
APP_ROUTER_APPS_ENABLED=0
APP_ROUTER_VIDEO_ENABLED=0
APP_ROUTER_TEAM_ENABLED=0
APP_ROUTER_TEAMS_ENABLED=0
APP_ROUTER_AVAILABILITY_ENABLED=0
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
APP_ROUTER_AUTH_LOGIN_ENABLED=0
APP_ROUTER_AUTH_LOGOUT_ENABLED=0
APP_ROUTER_AUTH_NEW_ENABLED=0
APP_ROUTER_AUTH_SAML_ENABLED=0
APP_ROUTER_AUTH_ERROR_ENABLED=0
APP_ROUTER_AUTH_PLATFORM_ENABLED=0
APP_ROUTER_AUTH_OAUTH2_ENABLED=0
APP_ROUTER_MORE_ENABLED=0
APP_ROUTER_MAINTENANCE_ENABLED=0
APP_ROUTER_UPGRADE_ENABLED=0
APP_ROUTER_CONNECT_AND_JOIN_ENABLED=0
# disable setry server source maps
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
@@ -362,10 +379,22 @@ TASKER_ENABLE_EMAILS=0
# Ratelimiting via unkey
UNKEY_ROOT_KEY=
# Used for Cal.ai Enterprise Voice AI Agents
# https://retellai.com
RETELL_AI_KEY=
# Used to disallow emails as being added as guests on bookings
BLACKLISTED_GUEST_EMAILS=
# Used to allow browser push notifications
# You can use: 'npx web-push generate-vapid-keys' to generate these keys
NEXT_PUBLIC_VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
# Custom privacy policy / terms URLs (for self-hosters: change to your privacy policy / terms URLs)
NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL=
NEXT_PUBLIC_WEBSITE_TERMS_URL=
# NEXT_PUBLIC_LOGGER_LEVEL=3 sets to log info, warn, error and fatal logs.
# [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal]
NEXT_PUBLIC_LOGGER_LEVEL=
+39
View File
@@ -0,0 +1,39 @@
---
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
@@ -1,81 +0,0 @@
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?
+5
View File
@@ -34,6 +34,11 @@ jobs:
uses: ./.github/workflows/api-v2-production-build.yml
secrets: inherit
build-atoms:
name: Production builds
uses: ./.github/workflows/atoms-production-build.yml
secrets: inherit
build:
name: Production builds
uses: ./.github/workflows/production-build-without-database.yml
@@ -0,0 +1,40 @@
name: Atoms production Build
on:
workflow_call:
jobs:
build:
name: Build atoms
permissions:
contents: read
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- name: Cache atoms production build
uses: buildjet/cache@v4
id: cache-atoms-build
env:
cache-name: atoms-build
key-1: ${{ hashFiles('yarn.lock') }}
key-2: ${{ hashFiles('packages/platform/atoms/**.[jt]s', 'packages/platform/atoms/**.[jt]sx', '!**/node_modules') }}
key-3: ${{ github.event.pull_request.number || github.ref }}
# Ensures production-build.yml will always be fresh
key-4: ${{ github.sha }}
with:
path: |
**/dist/**
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
- run: |
export NODE_OPTIONS="--max_old_space_size=8192"
if [ ${{ steps.cache-atoms-build.outputs.cache-hit }} == 'true' ]; then
echo "Cache hit for atoms build. Skipping build."
else
rm -rf packages/platform/atoms/node_modules
yarn install
yarn workspace @calcom/atoms run build
fi
shell: bash
+7
View File
@@ -4,6 +4,11 @@ on:
delete:
branches-ignore: [main, gh-pages]
permissions:
contents: write
issues: write
pull-requests: write
# ensures that currently running Playwright workflow of deleted branch gets cancelled
concurrency:
group: ${{ github.event.ref }}
@@ -20,7 +25,9 @@ jobs:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v2
with:
repository: calcom/test-results
ref: gh-pages
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Set Git User
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
run: |
-42
View File
@@ -1,42 +0,0 @@
name: Crowdin Action
on:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: crowdin action
uses: crowdin/github-action@v1.13.0
with:
# upload sources
upload_sources: true
# upload translations (& auto-approve 'em)
upload_translations_args: '--auto-approve-imported'
upload_translations: true
push_translations: true
# download translations
download_translations: true
# GH config
commit_message: "New Crowdin translations by Github Action"
localization_branch_name: main
create_pull_request: false
env:
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
+3
View File
@@ -45,6 +45,9 @@ jobs:
- 5432:5432
redis:
image: redis:latest
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
ports:
- 6379:6379
options: >-
+25
View File
@@ -0,0 +1,25 @@
name: Run i18n AI automation
on:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-main
cancel-in-progress: false
jobs:
i18n:
name: Run i18n
runs-on: buildjet-2vcpu-ubuntu-2204
permissions:
actions: write
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: replexica/replexica@main
env:
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
with:
api-key: ${{ secrets.CI_REPLEXICA_API_KEY }}
pull-request: true
-20
View File
@@ -1,20 +0,0 @@
name: PR Reviewed
on:
pull_request_review:
types: [submitted]
jobs:
label-pr:
runs-on: ubuntu-latest
permissions:
actions: read
issues: write
pull-requests: write
steps:
- name: Label PR as ready for E2E
if: github.event.review.state == 'approved'
uses: actions-ecosystem/action-add-labels@v1
with:
github_token: ${{ secrets.READY_FOR_E2E_PAT }}
labels: 'ready-for-e2e'
+7
View File
@@ -132,6 +132,13 @@ jobs:
uses: ./.github/workflows/api-v2-production-build.yml
secrets: inherit
build-atoms:
name: Production builds
needs: [changes, check-label, deps]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/atoms-production-build.yml
secrets: inherit
build:
name: Production builds
needs: [changes, check-label, deps]
+3 -1
View File
@@ -16,7 +16,9 @@ jobs:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v4
with:
repository: calcom/test-results
ref: gh-pages
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Set Git User
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
run: |
@@ -65,7 +67,7 @@ jobs:
- name: Output Report URL as Workflow Annotation
id: url
run: |
FULL_HTML_REPORT_URL=https://calcom.github.io/cal.com/$HTML_REPORT_URL_PATH
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results/$HTML_REPORT_URL_PATH
echo "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
- name: Find Comment
File diff suppressed because one or more lines are too long
+20 -2
View File
@@ -173,11 +173,29 @@ yarn dx
#### Development tip
> Add `NEXT_PUBLIC_DEBUG=1` anywhere in your `.env` to get logging information for all the queries and mutations driven by **tRPC**.
Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
Where {level} can be one of the following:
`0` for silly \
`1` for trace \
`2` for debug \
`3` for info \
`4` for warn \
`5` for error \
`6` for fatal
When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works:
The logger will include all logs that are at the specified level or higher. For example: \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \
```sh
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
echo 'NEXT_PUBLIC_LOGGER_LEVEL=3' >> .env
```
for Logger level to be set at info, for example.
#### Gitpod Setup
+31 -1
View File
@@ -1,6 +1,13 @@
import { z } from "zod";
import { _AttendeeModel, _BookingModel as Booking, _PaymentModel, _UserModel } from "@calcom/prisma/zod";
import {
_AttendeeModel,
_BookingModel as Booking,
_EventTypeModel,
_PaymentModel,
_TeamModel,
_UserModel,
} from "@calcom/prisma/zod";
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";
import { schemaQueryUserId } from "./shared/queryUserId";
@@ -14,6 +21,8 @@ const schemaBookingBaseBodyParams = Booking.pick({
startTime: true,
endTime: true,
status: true,
rescheduledBy: true,
cancelledBy: true,
}).partial();
export const schemaBookingCreateBodyParams = extendedBookingCreateBody.merge(schemaQueryUserId.partial());
@@ -33,6 +42,8 @@ const schemaBookingEditParams = z
title: z.string().optional(),
startTime: iso8601.optional(),
endTime: iso8601.optional(),
cancelledBy: z.string().email({ message: "Invalid Email" }).optional(),
rescheduledBy: z.string().email({ message: "Invalid Email" }).optional(),
// Not supporting responses in edit as that might require re-triggering emails
// responses
})
@@ -42,7 +53,23 @@ export const schemaBookingEditBodyParams = schemaBookingBaseBodyParams
.merge(schemaBookingEditParams)
.omit({ uid: true });
const teamSchema = _TeamModel.pick({
name: true,
slug: true,
});
export const schemaBookingReadPublic = Booking.extend({
eventType: _EventTypeModel
.pick({
title: true,
slug: true,
})
.merge(
z.object({
team: teamSchema.nullish(),
})
)
.nullish(),
attendees: z
.array(
_AttendeeModel.pick({
@@ -83,9 +110,12 @@ export const schemaBookingReadPublic = Booking.extend({
timeZone: true,
attendees: true,
user: true,
eventType: true,
payment: true,
metadata: true,
status: true,
responses: true,
fromReschedule: true,
cancelledBy: true,
rescheduledBy: true,
});
@@ -168,6 +168,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
metadata: jsonSchema.nullable(),
customInputs: customInputSchema.array().optional(),
link: z.string().optional(),
hashedLink: z.object({ link: z.string() }).optional().nullable(),
bookingFields: eventTypeBookingFields.optional().nullable(),
})
);
@@ -0,0 +1,12 @@
import { z } from "zod";
const expandEnum = z.enum(["team"]);
export const schemaQuerySingleOrMultipleExpand = z
.union([
expandEnum, // Allow a single value from the enum
z.array(expandEnum).refine((arr) => new Set(arr).size === arr.length, {
message: "Array values must be unique",
}), // Allow an array of enum values, with uniqueness constraint
])
.optional();
+1 -1
View File
@@ -30,7 +30,7 @@ enum locales {
RO = "ro",
NL = "nl",
PT_BR = "pt-BR",
// ES_419 = "es-419", // Disabled until Crowdin reaches at least 80% completion
ES_419 = "es-419",
KO = "ko",
JA = "ja",
PL = "pl",
@@ -30,40 +30,59 @@ async function authMiddleware(req: NextApiRequest) {
}
}
const userWithBookingsAndTeamIds = await prisma.user.findUnique({
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
select: {
email: true,
bookings: true,
teams: {
select: {
teamId: true,
},
});
if (!user) throw new HttpError({ statusCode: 404, message: "User not found" });
const filteredBookings = user?.bookings?.filter((booking) => booking.id === id);
const userIsHost = !!filteredBookings?.length;
const bookingsAsAttendee = prisma.booking.findMany({
where: {
id,
attendees: { some: { email: user.email } },
},
});
const bookingsAsEventTypeOwner = prisma.booking.findMany({
where: {
id,
eventType: {
owner: { id: userId },
},
},
});
const bookingsAsTeamOwnerOrAdmin = prisma.booking.findMany({
where: {
id,
eventType: {
team: {
members: {
some: { userId, role: { in: ["ADMIN", "OWNER"] }, accepted: true },
},
},
},
},
});
if (!userWithBookingsAndTeamIds) throw new HttpError({ statusCode: 404, message: "User not found" });
const [resultOne, resultTwo, resultThree] = await Promise.all([
bookingsAsAttendee,
bookingsAsEventTypeOwner,
bookingsAsTeamOwnerOrAdmin,
]);
const userBookingIds = userWithBookingsAndTeamIds.bookings.map((booking) => booking.id);
const teamBookingsAsOwnerOrAdmin = [...resultOne, ...resultTwo, ...resultThree];
const userHasTeamBookings = !!teamBookingsAsOwnerOrAdmin.length;
if (!userBookingIds.includes(id)) {
const teamBookings = await prisma.booking.findUnique({
where: {
id: id,
eventType: {
team: {
id: {
in: userWithBookingsAndTeamIds.teams.map((team) => team.teamId),
},
},
},
},
});
if (!teamBookings) {
throw new HttpError({ statusCode: 403, message: "You are not authorized" });
}
}
if (!userIsHost && !userHasTeamBookings)
throw new HttpError({ statusCode: 403, message: "You are not authorized" });
}
export default authMiddleware;
+14 -1
View File
@@ -4,6 +4,7 @@ import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { schemaBookingReadPublic } from "~/lib/validations/booking";
import { schemaQuerySingleOrMultipleExpand } from "~/lib/validations/shared/queryExpandRelations";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
/**
@@ -89,9 +90,21 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
export async function getHandler(req: NextApiRequest) {
const { query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const queryFilterForExpand = schemaQuerySingleOrMultipleExpand.parse(req.query.expand);
const expand = Array.isArray(queryFilterForExpand)
? queryFilterForExpand
: queryFilterForExpand
? [queryFilterForExpand]
: [];
const booking = await prisma.booking.findUnique({
where: { id },
include: { attendees: true, user: true, payment: true },
include: {
attendees: true,
user: true,
payment: true,
eventType: expand.includes("team") ? { include: { team: true } } : false,
},
});
return { booking: schemaBookingReadPublic.parse(booking) };
}
+9
View File
@@ -12,6 +12,7 @@ import {
} from "~/lib/utils/retrieveScopedAccessibleUsers";
import { schemaBookingGetParams, schemaBookingReadPublic } from "~/lib/validations/booking";
import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail";
import { schemaQuerySingleOrMultipleExpand } from "~/lib/validations/shared/queryExpandRelations";
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
/**
@@ -216,10 +217,18 @@ export async function handler(req: NextApiRequest) {
args.take = take;
args.skip = skip;
}
const queryFilterForExpand = schemaQuerySingleOrMultipleExpand.parse(req.query.expand);
const expand = Array.isArray(queryFilterForExpand)
? queryFilterForExpand
: queryFilterForExpand
? [queryFilterForExpand]
: [];
args.include = {
attendees: true,
user: true,
payment: true,
eventType: expand.includes("team") ? { include: { team: true } } : false,
};
const queryFilterForAttendeeEmails = schemaQuerySingleOrMultipleAttendeeEmails.parse(req.query);
@@ -6,6 +6,9 @@ import prisma from "@calcom/prisma";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
import checkParentEventOwnership from "../_utils/checkParentEventOwnership";
import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission";
/**
* @swagger
* /event-types/{id}:
@@ -49,9 +52,18 @@ async function checkPermissions(req: NextApiRequest) {
const { userId, isSystemWideAdmin } = req;
const { id } = schemaQueryIdParseInt.parse(req.query);
if (isSystemWideAdmin) return;
/** Only event type owners can delete it */
const eventType = await prisma.eventType.findFirst({ where: { id, userId } });
const eventType = await prisma.eventType.findFirst({ where: { id } });
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
/** Only event type owners or team owners for team events can delete it */
if (eventType.teamId) return await checkTeamEventEditPermission(req, { teamId: eventType.teamId });
if (eventType.parentId) return await checkParentEventOwnership(req);
if (eventType.userId && eventType.userId !== userId)
throw new HttpError({ statusCode: 403, message: "Forbidden" });
}
export default defaultResponder(deleteHandler);
@@ -51,6 +51,7 @@ export async function getHandler(req: NextApiRequest) {
where: { id },
include: {
customInputs: true,
hashedLink: { select: { link: true } },
team: { select: { slug: true } },
hosts: { select: { userId: true, isFixed: true } },
owner: { select: { username: true, id: true } },
@@ -56,6 +56,7 @@ async function getHandler(req: NextApiRequest) {
},
include: {
customInputs: true,
hashedLink: { select: { link: true } },
team: { select: { slug: true } },
hosts: { select: { userId: true, isFixed: true } },
owner: { select: { username: true, id: true } },
@@ -18,12 +18,8 @@ export default async function checkParentEventOwnership(req: NextApiRequest) {
/** These are already parsed upstream, we can assume they're good here. */
const parentId = Number(body.parentId);
const parentEventType = await prisma.eventType.findUnique({
where: {
id: parentId,
},
select: {
teamId: true,
},
where: { id: parentId },
select: { teamId: true },
});
if (!parentEventType) {
@@ -44,7 +40,8 @@ export default async function checkParentEventOwnership(req: NextApiRequest) {
where: {
teamId: parentEventType.teamId,
userId: userId,
OR: [{ role: "OWNER" }, { role: "ADMIN" }],
role: { in: ["ADMIN", "OWNER"] },
accepted: true,
},
});
@@ -10,21 +10,17 @@ export default async function checkTeamEventEditPermission(
req: NextApiRequest,
body: Pick<z.infer<typeof schemaEventTypeCreateBodyParams>, "teamId" | "userId">
) {
const { isSystemWideAdmin } = req;
let userId = req.userId;
if (isSystemWideAdmin && body.userId) {
userId = body.userId;
}
if (body.teamId) {
const membership = await prisma.membership.findFirst({
where: {
userId,
userId: req.userId,
teamId: body.teamId,
accepted: true,
role: { in: ["ADMIN", "OWNER"] },
},
});
if (!membership?.role || !["ADMIN", "OWNER"].includes(membership.role)) {
if (!membership) {
throw new HttpError({
statusCode: 403,
message: "No permission to operate on event-type for this team",
+11 -2
View File
@@ -3,6 +3,7 @@ import utc from "dayjs/plugin/utc";
import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { isSupportedTimeZone } from "@calcom/lib/date-fns";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
@@ -17,14 +18,22 @@ import { getHTTPStatusCodeFromError } from "@trpc/server/http";
dayjs.extend(utc);
dayjs.extend(timezone);
let isColdStart = true;
async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { usernameList, ...rest } = req.query;
if (isColdStart && IS_PRODUCTION) {
console.log("Cold start of /v1/slots detected");
isColdStart = false;
}
const { usernameList, isTeamEvent, ...rest } = req.query;
const parsedIsTeamEvent = String(isTeamEvent).toLowerCase() === "true";
let slugs = usernameList;
if (!Array.isArray(usernameList)) {
slugs = usernameList ? [usernameList] : undefined;
}
const input = getScheduleSchema.parse({ usernameList: slugs, ...rest });
const input = getScheduleSchema.parse({ usernameList: slugs, isTeamEvent: parsedIsTeamEvent, ...rest });
const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
const availableSlots = await getAvailableSlots({ ctx: await createContext({ req, res }), input });
const slotsInProvidedTimeZone = timeZoneSupported
@@ -41,7 +41,7 @@ export async function canUserAccessTeamWithRole(
/** If not ADMIN then we check if the actual user belongs to team and matches the required role */
if (!isSystemWideAdmin) args.where = { ...args.where, members: { some: { userId, role } } };
const team = await prisma.team.findFirst(args);
if (!team) throw new HttpError({ statusCode: 401, message: `Unauthorized: ${role.toString()} required` });
if (!team) throw new HttpError({ statusCode: 401, message: `Unauthorized: OWNER or ADMIN role required` });
return team;
}
+1
View File
@@ -215,6 +215,7 @@ const generateTeamCheckoutSession = async ({
metadata: {
pendingPaymentTeamId,
ownerId,
dubCustomerId: ownerId, // pass the userId during checkout creation for sales conversion tracking: https://d.to/conversions/stripe
},
});
-3
View File
@@ -1,6 +1,3 @@
# Set the version of docker compose to use
version: "3.9"
# The containers that compose the project
services:
db:
@@ -1,90 +1,332 @@
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, it, expect } from "vitest";
import { describe, it, expect, test } from "vitest";
import type { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import authMiddleware from "../../../pages/api/bookings/[id]/_auth-middleware";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
describe("Bookings auth middleware", () => {
it("Returns 403 when user has no permission to the booking", async () => {
const trialUser = await prisma.user.findFirstOrThrow({ where: { email: "trial@example.com" } });
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: booking.id,
describe("Booking ownership and access in Middleware", () => {
const adminUserId = 1;
const ownerUserId = 2;
const memberUserId = 3;
const orgOwnerUserId = 4;
const adminUserEmail = "admin@example.com";
const ownerUserEmail = "owner@example.com";
const memberUserEmail = "member@example.com";
const orgOwnerUserEmail = "org-owner@example.com";
// mock user data
function buildMockData() {
//Create Users
prismock.user.create({
data: {
id: adminUserId,
username: "admin",
name: "Admin User",
email: adminUserEmail,
},
});
prismock.user.create({
data: {
id: ownerUserId,
username: "owner",
name: "Owner User",
email: ownerUserEmail,
},
});
prismock.user.create({
data: {
id: orgOwnerUserId,
username: "org-owner",
name: "Org Owner",
email: orgOwnerUserEmail,
},
});
prismock.user.create({
data: {
id: memberUserId,
username: "member",
name: "Member User",
email: memberUserEmail,
bookings: {
create: {
id: 2,
uid: "2",
title: "Booking 2",
eventTypeId: 1,
startTime: "2024-08-30T06:45:00.000Z",
endTime: "2024-08-30T07:45:00.000Z",
attendees: {
create: {
name: "Member User",
email: memberUserEmail,
timeZone: "UTC",
},
},
},
},
},
});
//create team
prismock.team.create({
data: {
id: 1,
name: "Team 1",
slug: "team1",
members: {
createMany: {
data: [
{
userId: adminUserId,
role: MembershipRole.ADMIN,
accepted: true,
},
{
userId: ownerUserId,
role: MembershipRole.OWNER,
accepted: true,
},
{
userId: memberUserId,
role: MembershipRole.MEMBER,
accepted: true,
},
],
},
},
},
});
//create Org
prismock.team.create({
data: {
id: 2,
name: "Org",
slug: "org",
isOrganization: true,
children: {
connect: {
id: 1,
},
},
members: {
createMany: {
data: [
{
userId: orgOwnerUserId,
role: MembershipRole.OWNER,
accepted: true,
},
{
userId: memberUserId,
role: MembershipRole.MEMBER,
accepted: true,
},
{
userId: ownerUserId,
role: MembershipRole.MEMBER,
accepted: true,
},
{
userId: adminUserId,
role: MembershipRole.MEMBER,
accepted: true,
},
],
},
},
},
});
//create eventTypes
prismock.eventType.create({
data: {
id: 1,
title: "Event 1",
slug: "event",
length: 60,
bookings: {
connect: {
id: 2,
},
},
},
});
prismock.eventType.create({
data: {
id: 2,
title: "Event 2",
slug: "event",
length: 60,
teamId: 1,
bookings: {
connect: {
id: 1,
},
},
},
});
//link eventType to teams
prismock.eventType.update({
where: {
id: 1,
},
data: {
owner: {
connect: {
id: ownerUserId,
},
},
},
});
prismock.eventType.update({
where: {
id: 2,
},
data: {
team: {
connect: {
id: 1,
},
},
},
});
//link team to org
prismock.team.update({
where: {
id: 1,
},
data: {
parentId: 2,
},
});
// Call Prisma to create booking with attendees
prismock.booking.create({
data: {
id: 1,
uid: "1",
title: "Booking 1",
userId: 1,
startTime: "2024-08-30T06:45:00.000Z",
endTime: "2024-08-30T07:45:00.000Z",
eventTypeId: 2,
attendees: {
create: {
name: "Admin User",
email: adminUserEmail,
timeZone: "UTC",
},
},
},
});
}
req.userId = trialUser.id;
try {
await authMiddleware(req);
} catch (error) {
const httpError = error as HttpError;
expect(httpError.statusCode).toBe(403);
}
test("should not throw error for bookings where user is an attendee", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
id: 2,
},
prisma: prismock,
});
buildMockData();
req.userId = memberUserId;
await expect(authMiddleware(req)).resolves.not.toThrow();
});
it("No error is thrown when user is the booking user", async () => {
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
test("should throw error for bookings where user is not an attendee", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: booking.id,
id: 1,
},
prisma: prismock,
});
buildMockData();
req.userId = memberUserId;
req.userId = proUser.id;
await authMiddleware(req);
await expect(authMiddleware(req)).rejects.toThrow();
});
it("No error is thrown when user is system-wide admin", async () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "admin@example.com" } });
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
test("should not throw error for booking where user is the event type owner", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: booking.id,
id: 2,
},
prisma: prismock,
});
buildMockData();
req.userId = ownerUserId;
await expect(authMiddleware(req)).resolves.not.toThrow();
});
req.userId = adminUser.id;
test("should not throw error for booking where user is team owner or admin", async () => {
const { req: req1 } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
id: 1,
},
prisma: prismock,
});
const { req: req2 } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
id: 1,
},
prisma: prismock,
});
buildMockData();
req1.userId = adminUserId;
req2.userId = ownerUserId;
await expect(authMiddleware(req1)).resolves.not.toThrow();
await expect(authMiddleware(req2)).resolves.not.toThrow();
});
test("should throw error for booking where user is not team owner or admin", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
id: 1,
},
prisma: prismock,
});
buildMockData();
req.userId = memberUserId;
await expect(authMiddleware(req)).rejects.toThrow();
});
test("should not throw error when user is system-wide admin", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
id: 2,
},
prisma: prismock,
});
buildMockData();
req.userId = adminUserId;
req.isSystemWideAdmin = true;
await authMiddleware(req);
});
it("No error is thrown when user is org-wide admin", async () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member1-acme@example.com" } });
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: memberUser.id } });
it("should throw error when user is org-wide admin", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: booking.id,
id: 1,
},
prisma: prismock,
});
req.userId = adminUser.id;
buildMockData();
req.userId = orgOwnerUserId;
req.isOrganizationOwnerOrAdmin = true;
await authMiddleware(req);
@@ -164,7 +164,6 @@ describe("GET /api/bookings", async () => {
const responseData = await handler(req);
responseData.bookings.forEach((booking) => {
console.log(booking);
expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual(new Date().getTime());
});
});
@@ -188,4 +187,27 @@ describe("GET /api/bookings", async () => {
});
});
});
describe("Expand feature to add relational data in return payload", () => {
it("Returns only team data when expand=team is set", async () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
expand: "team",
},
pagination: DefaultPagination,
});
req.userId = adminUser.id;
req.isOrganizationOwnerOrAdmin = true;
const responseData = await handler(req);
console.log("bookings=>", responseData.bookings);
responseData.bookings.forEach((booking) => {
if (booking.id === 31) expect(booking.eventType?.team?.slug).toBe("team1");
if (booking.id === 19) expect(booking.eventType?.team).toBe(null);
});
});
});
});
@@ -0,0 +1,94 @@
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, beforeEach, vi } from "vitest";
import { buildEventType } from "@calcom/lib/test/builder";
import { MembershipRole } from "@calcom/prisma/enums";
import handler from "../../../../pages/api/event-types/[id]/_delete";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
describe("DELETE /api/event-types/[id]", () => {
const eventTypeId = 1234567;
const teamId = 9999;
const adminUser = 1111;
const memberUser = 2222;
beforeEach(() => {
vi.resetAllMocks();
// Mocking membership.findFirst
prismaMock.membership.findFirst.mockImplementation(({ where }) => {
const { userId, teamId, accepted, role } = where;
const mockData = [
{ userId: 1111, teamId: teamId, accepted: true, role: MembershipRole.ADMIN },
{ userId: 2222, teamId: teamId, accepted: true, role: MembershipRole.MEMBER },
];
// Return the correct user based on the query conditions
return mockData.find(
(membership) =>
membership.userId === userId &&
membership.teamId === teamId &&
membership.accepted === accepted &&
role.in.includes(membership.role)
);
});
// Mocking eventType.findFirst
prismaMock.eventType.findFirst.mockResolvedValue(
buildEventType({
id: eventTypeId,
teamId,
})
);
// Mocking team.findUnique
prismaMock.team.findUnique.mockResolvedValue({
id: teamId,
members: [
{ userId: memberUser, role: MembershipRole.MEMBER, teamId: teamId },
{ userId: adminUser, role: MembershipRole.ADMIN, teamId: teamId },
],
});
});
describe("Error", async () => {
test("Fails to remove event type if user is not OWNER/ADMIN of team associated with event type", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "DELETE",
body: {},
query: {
id: eventTypeId,
},
});
// Assign userId to the request objects
req.userId = memberUser;
await handler(req, res);
expect(res.statusCode).toBe(403); // Check if the deletion was successful
});
});
describe("Success", async () => {
test("Removes event type if user is owner of team associated with event type", async () => {
// Mocks for DELETE request
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "DELETE",
body: {},
query: {
id: eventTypeId,
},
});
// Assign userId to the request objects
req.userId = adminUser;
await handler(req, res);
expect(res.statusCode).toBe(200); // Check if the deletion was successful
});
});
});
-2
View File
@@ -1,5 +1,3 @@
version: '3.8'
services:
redis:
image: redis:latest
+5 -2
View File
@@ -10,8 +10,9 @@
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"dev:build:watch": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch",
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build",
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build ",
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && yarn start --watch",
"dev:no-docker": "yarn dev:build && yarn copy-swagger-module && yarn start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node ./dist/apps/api/v2/src/main.js",
"test": "yarn dev:build && jest",
@@ -19,13 +20,15 @@
"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 --detectOpenHandles --forceExit --config ./jest-e2e.json",
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch",
"prisma": "yarn workspace @calcom/prisma prisma",
"generate-schemas": "yarn prisma generate && yarn prisma format",
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts"
},
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.28",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.36",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
+4 -2
View File
@@ -1,4 +1,5 @@
import appConfig from "@/config/app";
import { CustomThrottlerGuard } from "@/lib/throttler-guard";
import { AppLoggerMiddleware } from "@/middleware/app.logger.middleware";
import { RewriterMiddleware } from "@/middleware/app.rewrites.middleware";
import { JsonBodyMiddleware } from "@/middleware/body/json.body.middleware";
@@ -15,7 +16,7 @@ import { BullModule } from "@nestjs/bull";
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";
import { seconds, ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
import { seconds, ThrottlerModule } from "@nestjs/throttler";
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";
import { AppController } from "./app.controller";
@@ -32,6 +33,7 @@ import { AppController } from "./app.controller";
BullModule.forRoot({
redis: `${process.env.REDIS_URL}${process.env.NODE_ENV === "production" ? "?tls=true" : ""}`,
}),
// Rate limiting here is handled by the CustomThrottlerGuard
ThrottlerModule.forRootAsync({
imports: [RedisModule],
inject: [RedisService],
@@ -59,7 +61,7 @@ import { AppController } from "./app.controller";
},
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
useClass: CustomThrottlerGuard,
},
],
})
@@ -1,4 +1,5 @@
import { BookingsController } from "@/ee/bookings/controllers/bookings.controller";
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
import { BillingModule } from "@/modules/billing/billing.module";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
@@ -10,7 +11,7 @@ import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, RedisModule, TokensModule, BillingModule],
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository],
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository, ApiKeyRepository],
controllers: [BookingsController],
})
export class BookingsModule {}
@@ -14,6 +14,7 @@ import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
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";
@@ -21,7 +22,7 @@ import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { handleNewBooking } from "@calcom/platform-libraries";
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
import { ApiSuccessResponse, ApiResponse, ApiErrorResponse } from "@calcom/platform-types";
describe("Bookings Endpoints", () => {
describe("User Authenticated", () => {
@@ -31,6 +32,8 @@ describe("Bookings Endpoints", () => {
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let apiKeyString: string;
const userEmail = "bookings-controller-e2e@api.com";
let user: User;
@@ -56,11 +59,13 @@ describe("Bookings Endpoints", () => {
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
});
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = keyString;
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
@@ -139,6 +144,105 @@ describe("Bookings Endpoints", () => {
});
});
it("should fail to create a booking with no_available_users_found_error", async () => {
const bookingStart = "2040-05-21T09:30:00.000Z";
const bookingEnd = "2040-05-21T10:30:00.000Z";
const bookingEventTypeId = eventTypeId;
const bookingTimeZone = "Europe/London";
const bookingLanguage = "en";
const bookingHashedLink = "";
const bookingMetadata = {
timeFormat: "12",
meetingType: "organizer-phone",
};
const bookingResponses = {
name: "tester",
email: "tester@example.com",
location: {
value: "link",
optionValue: "",
},
notes: "test",
guests: [],
};
const body: CreateBookingInput = {
start: bookingStart,
end: bookingEnd,
eventTypeId: bookingEventTypeId,
timeZone: bookingTimeZone,
language: bookingLanguage,
metadata: bookingMetadata,
hashedLink: bookingHashedLink,
responses: bookingResponses,
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.expect(400)
.then(async (response) => {
const responseBody: ApiErrorResponse = response.body;
expect(responseBody.error.message).toEqual("no_available_users_found_error");
});
});
it("should create a booking with api key to get owner id", async () => {
const bookingStart = "2040-05-22T09:30:00.000Z";
const bookingEnd = "2040-05-22T10:30:00.000Z";
const bookingEventTypeId = eventTypeId;
const bookingTimeZone = "Europe/London";
const bookingLanguage = "en";
const bookingHashedLink = "";
const bookingMetadata = {
timeFormat: "12",
meetingType: "organizer-phone",
};
const bookingResponses = {
name: "tester",
email: "tester@example.com",
location: {
value: "link",
optionValue: "",
},
notes: "test",
guests: [],
};
const body: CreateBookingInput = {
start: bookingStart,
end: bookingEnd,
eventTypeId: bookingEventTypeId,
timeZone: bookingTimeZone,
language: bookingLanguage,
metadata: bookingMetadata,
hashedLink: bookingHashedLink,
responses: bookingResponses,
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.userPrimaryEmail).toBeDefined();
expect(responseBody.data.userPrimaryEmail).toEqual(userEmail);
expect(responseBody.data.id).toBeDefined();
expect(responseBody.data.uid).toBeDefined();
expect(responseBody.data.startTime).toEqual(bookingStart);
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
expect(responseBody.data.metadata).toEqual(bookingMetadata);
createdBooking = responseBody.data;
});
});
describe("should reschedule a booking", () => {
it("should reschedule with updated start time, end time & metadata", async () => {
const newBookingStart = "2040-05-21T12:30:00.000Z";
@@ -202,18 +306,22 @@ describe("Bookings Endpoints", () => {
.get("/v2/bookings?filters[status]=upcoming")
.then((response) => {
const responseBody: GetBookingsOutput = response.body;
const fetchedBooking = responseBody.data.bookings[0];
expect(responseBody.data.bookings.length).toEqual(1);
expect(responseBody.data.bookings.length).toEqual(2);
const fetchedBooking = responseBody.data.bookings.find(
(booking) => booking.id === createdBooking.id
);
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(fetchedBooking).toBeDefined();
expect(fetchedBooking.id).toEqual(createdBooking.id);
expect(fetchedBooking.uid).toEqual(createdBooking.uid);
expect(fetchedBooking.startTime).toEqual(createdBooking.startTime);
expect(fetchedBooking.endTime).toEqual(createdBooking.endTime);
expect(fetchedBooking.user?.email).toEqual(userEmail);
if (fetchedBooking) {
expect(fetchedBooking.id).toEqual(createdBooking.id);
expect(fetchedBooking.uid).toEqual(createdBooking.uid);
expect(fetchedBooking.startTime).toEqual(createdBooking.startTime);
expect(fetchedBooking.endTime).toEqual(createdBooking.endTime);
expect(fetchedBooking.user?.email).toEqual(userEmail);
}
});
});
@@ -4,7 +4,9 @@ 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 { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
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";
@@ -28,10 +30,12 @@ import {
NotFoundException,
UseGuards,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { Request } from "express";
import { NextApiRequest } from "next/types";
import { v4 as uuidv4 } from "uuid";
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants";
@@ -46,6 +50,7 @@ import {
getBookingInfo,
handleCancelBooking,
getBookingForReschedule,
ErrorCode,
} from "@calcom/platform-libraries";
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
import { ApiResponse } from "@calcom/platform-types";
@@ -86,7 +91,9 @@ export class BookingsController {
private readonly oAuthFlowService: OAuthFlowService,
private readonly prismaReadService: PrismaReadService,
private readonly oAuthClientRepository: OAuthClientRepository,
private readonly billingService: BillingService
private readonly billingService: BillingService,
private readonly config: ConfigService,
private readonly apiKeyRepository: ApiKeyRepository
) {}
@Get("/")
@@ -238,8 +245,15 @@ export class BookingsController {
): Promise<ApiResponse<BookingResponse[]>> {
const oAuthClientId = clientId?.toString();
try {
const recurringEventId = uuidv4();
for (const recurringEvent of req.body) {
if (!recurringEvent.recurringEventId) {
recurringEvent.recurringEventId = recurringEventId;
}
}
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
await this.createNextApiBookingRequest(req, oAuthClientId)
await this.createNextApiRecurringBookingRequest(req, oAuthClientId)
);
createdBookings.forEach(async (booking) => {
@@ -296,9 +310,17 @@ export class BookingsController {
private async getOwnerId(req: Request): Promise<number | undefined> {
try {
const accessToken = req.get("Authorization")?.replace("Bearer ", "");
if (accessToken) {
return this.oAuthFlowService.getOwnerId(accessToken);
const bearerToken = req.get("Authorization")?.replace("Bearer ", "");
if (bearerToken) {
if (isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")) {
const strippedApiKey = stripApiKey(bearerToken, this.config.get<string>("api.keyPrefix"));
const apiKeyHash = hashAPIKey(strippedApiKey);
const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash);
return keyData?.userId;
} else {
// Access Token
return this.oAuthFlowService.getOwnerId(bearerToken);
}
}
} catch (err) {
this.logger.error(err);
@@ -338,6 +360,19 @@ export class BookingsController {
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
private async createNextApiRecurringBookingRequest(
req: BookingRequest,
oAuthClientId?: string,
platformBookingLocation?: string
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
const userId = (await this.getOwnerId(req)) ?? -1;
const oAuthParams = oAuthClientId
? await this.getOAuthClientsParams(oAuthClientId)
: DEFAULT_PLATFORM_PARAMS;
Object.assign(req, { userId, ...oAuthParams, platformBookingLocation });
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
private handleBookingErrors(
err: Error | HttpError | unknown,
type?: "recurring" | `instant` | "no-show"
@@ -353,6 +388,9 @@ export class BookingsController {
if (err instanceof Error) {
const error = err as Error;
if (Object.values(ErrorCode).includes(error.message as unknown as ErrorCode)) {
throw new HttpException(error.message, 400);
}
throw new InternalServerErrorException(error?.message ?? errMsg);
}
@@ -59,10 +59,6 @@ export class CreateBookingInput {
@IsOptional()
rescheduleUid?: string;
@IsString()
@IsOptional()
recurringEventId?: string;
@IsTimeZone()
timeZone!: string;
@@ -1,5 +1,5 @@
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
import { IsBoolean, IsString, IsNumber, IsOptional } from "class-validator";
import type { AppsStatus } from "@calcom/platform-libraries";
@@ -21,4 +21,8 @@ export class CreateRecurringBookingInput extends CreateBookingInput {
@IsOptional()
@IsNumber()
currentRecurringIndex?: number;
@IsString()
@IsOptional()
recurringEventId?: string;
}
@@ -5,7 +5,8 @@ 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";
import { symmetricEncrypt, CalendarService, symmetricDecrypt } from "@calcom/platform-libraries";
import { Credential } from "@calcom/prisma/client";
@Injectable()
export class AppleCalendarService implements CredentialSyncCalendarApp {
@@ -61,26 +62,70 @@ export class AppleCalendarService implements CredentialSyncCalendarApp {
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,
};
const existingAppleCalendarCredentials = await this.credentialRepository.getAllUserCredentialsByTypeAndId(
APPLE_CALENDAR_TYPE,
userId
);
let hasMatchingUsernameAndPassword = false;
if (existingAppleCalendarCredentials.length > 0) {
const hasCalendarWithGivenCredentials = existingAppleCalendarCredentials.find(
(calendarCredential: Credential) => {
const decryptedKey = JSON.parse(
symmetricDecrypt(calendarCredential.key as string, process.env.CALENDSO_ENCRYPTION_KEY || "")
);
if (decryptedKey.username === username) {
if (decryptedKey.password === password) {
hasMatchingUsernameAndPassword = true;
}
return true;
}
}
);
if (!!hasCalendarWithGivenCredentials && hasMatchingUsernameAndPassword) {
return {
status: SUCCESS_STATUS,
};
}
if (!!hasCalendarWithGivenCredentials && !hasMatchingUsernameAndPassword) {
await this.credentialRepository.upsertAppCredential(
APPLE_CALENDAR_TYPE,
symmetricEncrypt(JSON.stringify({ username, password }), process.env.CALENDSO_ENCRYPTION_KEY || ""),
userId,
hasCalendarWithGivenCredentials.id
);
return {
status: SUCCESS_STATUS,
};
}
}
try {
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,
};
const dav = new CalendarService({
id: 0,
...data,
user: { email: userEmail },
});
await dav?.listCalendars();
await this.credentialRepository.createAppCredential(APPLE_CALENDAR_TYPE, data.key, userId);
await this.credentialRepository.upsertAppCredential(APPLE_CALENDAR_TYPE, data.key, userId);
} catch (reason) {
throw new BadRequestException(`Could not add this apple calendar account: ${reason}`);
}
@@ -6,6 +6,7 @@ import {
} from "@/modules/credentials/credentials.repository";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import {
Injectable,
@@ -15,9 +16,11 @@ import {
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { User } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { DateTime } from "luxon";
import { z } from "zod";
import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries";
import { Calendar } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
@@ -33,7 +36,8 @@ export class CalendarsService {
private readonly calendarsRepository: CalendarsRepository,
private readonly dbRead: PrismaReadService,
private readonly dbWrite: PrismaWriteService,
private readonly config: ConfigService
private readonly config: ConfigService,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
) {}
async getCalendars(userId: number) {
@@ -145,4 +149,32 @@ export class CalendarsService {
throw new NotFoundException("Calendar credentials not found");
}
}
async createAndLinkCalendarEntry(
userId: number,
externalId: string,
key: Prisma.InputJsonValue,
calendarType: keyof typeof APPS_TYPE_ID_MAPPING,
credentialId?: number | null
) {
const credential = await this.credentialsRepository.upsertAppCredential(
calendarType,
key,
userId,
credentialId
);
await this.selectedCalendarsRepository.upsertSelectedCalendar(
externalId,
credential.id,
userId,
calendarType
);
}
async checkCalendarCredentialValidity(userId: number, credentialId: number, type: string) {
const credential = await this.credentialsRepository.getUserCredentialById(userId, credentialId, type);
return !credential?.invalid;
}
}
@@ -131,11 +131,6 @@ export class GoogleCalendarService implements OAuthCalendarApp {
const token = await oAuth2Client.getToken(parsedCode);
// Google oAuth Credentials are stored in token.tokens
const key = token.tokens;
const credential = await this.credentialRepository.createAppCredential(
GOOGLE_CALENDAR_TYPE,
key as Prisma.InputJsonValue,
ownerId
);
oAuth2Client.setCredentials(key);
@@ -149,10 +144,39 @@ export class GoogleCalendarService implements OAuthCalendarApp {
const primaryCal = cals.data.items?.find((cal) => cal.primary);
if (primaryCal?.id) {
await this.selectedCalendarsRepository.createSelectedCalendar(
primaryCal.id,
credential.id,
const alreadyExistingSelectedCalendar = await this.selectedCalendarsRepository.getUserSelectedCalendar(
ownerId,
GOOGLE_CALENDAR_TYPE,
primaryCal.id
);
if (alreadyExistingSelectedCalendar) {
const isCredentialValid = await this.calendarsService.checkCalendarCredentialValidity(
ownerId,
alreadyExistingSelectedCalendar.credentialId ?? 0,
GOOGLE_CALENDAR_TYPE
);
// user credential probably got expired in this case
if (!isCredentialValid) {
await this.calendarsService.createAndLinkCalendarEntry(
ownerId,
alreadyExistingSelectedCalendar.externalId,
key as Prisma.InputJsonValue,
GOOGLE_CALENDAR_TYPE,
alreadyExistingSelectedCalendar.credentialId
);
}
return {
url: redir || origin,
};
}
await this.calendarsService.createAndLinkCalendarEntry(
ownerId,
primaryCal.id,
key as Prisma.InputJsonValue,
GOOGLE_CALENDAR_TYPE
);
}
@@ -168,16 +168,39 @@ export class OutlookService implements OAuthCalendarApp {
const defaultCalendar = await this.getDefaultCalendar(office365OAuthCredentials.access_token);
if (defaultCalendar?.id) {
const credential = await this.credentialRepository.createAppCredential(
const alreadyExistingSelectedCalendar = await this.selectedCalendarsRepository.getUserSelectedCalendar(
ownerId,
OFFICE_365_CALENDAR_TYPE,
office365OAuthCredentials,
ownerId
defaultCalendar.id
);
await this.selectedCalendarsRepository.createSelectedCalendar(
defaultCalendar.id,
credential.id,
if (alreadyExistingSelectedCalendar) {
const isCredentialValid = await this.calendarsService.checkCalendarCredentialValidity(
ownerId,
alreadyExistingSelectedCalendar.credentialId ?? 0,
OFFICE_365_CALENDAR_TYPE
);
// user credential probably got expired in this case
if (!isCredentialValid) {
await this.calendarsService.createAndLinkCalendarEntry(
ownerId,
alreadyExistingSelectedCalendar.externalId,
office365OAuthCredentials,
OFFICE_365_CALENDAR_TYPE,
alreadyExistingSelectedCalendar.credentialId
);
}
return {
url: redir || origin,
};
}
await this.calendarsService.createAndLinkCalendarEntry(
ownerId,
defaultCalendar.id,
office365OAuthCredentials,
OFFICE_365_CALENDAR_TYPE
);
}
@@ -36,7 +36,7 @@ import {
InternalServerErrorException,
ParseIntPipe,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
@@ -47,7 +47,7 @@ import { PrismaClient } from "@calcom/prisma";
version: [VERSION_2024_04_15, VERSION_2024_06_11],
})
@UseGuards(PermissionsGuard)
@DocsTags("Event types")
@DocsExcludeController(true)
export class EventTypesController_2024_04_15 {
constructor(
private readonly eventTypesService: EventTypesService_2024_04_15,
@@ -1,6 +1,7 @@
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 { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
@@ -9,7 +10,11 @@ import type { PrismaClient } from "@calcom/prisma";
@Injectable()
export class EventTypesRepository_2024_04_15 {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
constructor(
private readonly dbRead: PrismaReadService,
private readonly dbWrite: PrismaWriteService,
private usersService: UsersService
) {}
async createUserEventType(
userId: number,
@@ -46,7 +51,7 @@ export class EventTypesRepository_2024_04_15 {
eventTypeId: number
) {
return await getEventTypeById({
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
currentOrganizationId: this.usersService.getUserMainOrgId(user),
eventTypeId,
userId: user.id,
prisma: this.dbRead.prisma as unknown as PrismaClient,
@@ -6,6 +6,7 @@ import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs
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";
@@ -14,6 +15,7 @@ import {
updateEventType,
EventTypesPublic,
getEventTypesPublic,
systemBeforeFieldEmail,
} from "@calcom/platform-libraries";
import { EventType } from "@calcom/prisma/client";
@@ -24,7 +26,8 @@ export class EventTypesService_2024_04_15 {
private readonly membershipsRepository: MembershipsRepository,
private readonly usersRepository: UsersRepository,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
private readonly dbWrite: PrismaWriteService
private readonly dbWrite: PrismaWriteService,
private usersService: UsersService
) {}
async createUserEventType(
@@ -53,11 +56,11 @@ export class EventTypesService_2024_04_15 {
}
async getUserToCreateEvent(user: UserWithProfile) {
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
const organizationId = this.usersService.getUserMainOrgId(user);
const isOrgAdmin = organizationId
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
: false;
const profileId = user.movedToProfile?.id || null;
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
return {
id: user.id,
role: user.role,
@@ -80,7 +83,7 @@ export class EventTypesService_2024_04_15 {
}
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
const organizationId = this.usersService.getUserMainOrgId(user);
const isUserOrganizationAdmin = organizationId
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
@@ -125,8 +128,17 @@ export class EventTypesService_2024_04_15 {
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_04_15, user: UserWithProfile) {
this.checkCanUpdateEventType(user.id, eventTypeId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
const bookingFields = [...(body.bookingFields || [])];
if (
!bookingFields.find((field) => field.type === "email") &&
!bookingFields.find((field) => field.type === "phone")
) {
bookingFields.push(systemBeforeFieldEmail);
}
await updateEventType({
input: { id: eventTypeId, ...body },
input: { id: eventTypeId, ...body, bookingFields },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -153,7 +165,7 @@ export class EventTypesService_2024_04_15 {
}
async getUserToUpdateEvent(user: UserWithProfile) {
const profileId = user.movedToProfile?.id || null;
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
return { ...user, profile: { id: profileId }, selectedCalendars };
}
@@ -20,13 +20,13 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
import { BookingWindowPeriodInputTypeEnum_2024_06_14, FrequencyInput } from "@calcom/platform-enums";
import {
ApiSuccessResponse,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-types/dist/event-types/event-types_2024_06_14/inputs/enums/booking-window.enum";
import { SchedulingType } from "@calcom/prisma/enums";
describe("Event types Endpoints", () => {
@@ -230,6 +230,11 @@ describe("Event types Endpoints", () => {
value: 30,
rolling: true,
},
recurrence: {
frequency: FrequencyInput.weekly,
interval: 2,
occurrences: 10,
},
};
return request(app.getHttpServer())
@@ -245,7 +250,6 @@ describe("Event types Endpoints", () => {
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);
expect(createdEventType.bookingLimitsCount).toEqual(body.bookingLimitsCount);
@@ -253,6 +257,17 @@ describe("Event types Endpoints", () => {
expect(createdEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
expect(createdEventType.offsetStart).toEqual(body.offsetStart);
expect(createdEventType.bookingWindow).toEqual(body.bookingWindow);
expect(createdEventType.recurrence).toEqual(body.recurrence);
const responseBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", type: "name" },
{ isDefault: true, required: true, slug: "email", type: "email" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
...responseBookingFields.map((field) => ({ isDefault: false, ...field })),
];
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
eventType = responseBody.data;
});
});
@@ -278,6 +293,11 @@ describe("Event types Endpoints", () => {
value: 40,
rolling: false,
},
recurrence: {
frequency: FrequencyInput.monthly,
interval: 4,
occurrences: 10,
},
};
return request(app.getHttpServer())
@@ -303,6 +323,7 @@ describe("Event types Endpoints", () => {
expect(updatedEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
expect(updatedEventType.offsetStart).toEqual(body.offsetStart);
expect(updatedEventType.bookingWindow).toEqual(body.bookingWindow);
expect(updatedEventType.recurrence).toEqual(body.recurrence);
eventType.title = newTitle;
eventType.scheduleId = secondSchedule.id;
@@ -311,6 +332,7 @@ describe("Event types Endpoints", () => {
eventType.bookingLimitsDuration = updatedEventType.bookingLimitsDuration;
eventType.offsetStart = updatedEventType.offsetStart;
eventType.bookingWindow = updatedEventType.bookingWindow;
eventType.recurrence = updatedEventType.recurrence;
});
});
@@ -351,6 +373,7 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
});
it(`/GET/even-types by username`, async () => {
@@ -381,6 +404,7 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
});
it(`/GET/event-types by username and eventSlug`, async () => {
@@ -406,6 +430,7 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
});
it(`/GET/:id not existing`, async () => {
@@ -460,6 +485,16 @@ describe("Event types Endpoints", () => {
let legacyEventTypeId1: number;
let legacyEventTypeId2: number;
const expectedReturnSystemFields = [
{ isDefault: true, required: true, slug: "name", type: "name" },
{ isDefault: true, required: true, slug: "email", type: "email" },
{ isDefault: true, type: "radioInput", slug: "location", required: false },
{ isDefault: true, required: true, slug: "title", type: "text" },
{ isDefault: true, required: false, slug: "notes", type: "textarea" },
{ isDefault: true, required: false, slug: "guests", type: "multiemail" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
];
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
@@ -529,7 +564,7 @@ describe("Event types Endpoints", () => {
.expect(400);
});
it("should return empty bookingFields if system fields are the only one in database", async () => {
it("should return system bookingFields stored in database", async () => {
const legacyEventTypeInput = {
title: "legacy event type",
description: "legacy event type description",
@@ -622,11 +657,11 @@ describe("Event types Endpoints", () => {
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([]);
expect(fetchedEventType.bookingFields).toEqual(expectedReturnSystemFields);
});
});
it("should return user created bookingFields among system fields in the database", async () => {
it("should return user created bookingFields with system fields", async () => {
const userDefinedBookingField = {
name: "team",
type: "textarea",
@@ -739,7 +774,9 @@ describe("Event types Endpoints", () => {
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([
...expectedReturnSystemFields,
{
isDefault: false,
type: userDefinedBookingField.type,
slug: userDefinedBookingField.name,
label: userDefinedBookingField.label,
@@ -24,7 +24,7 @@ import {
Delete,
Query,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiTags as DocsTags } from "@nestjs/swagger";
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import {
@@ -39,12 +39,23 @@ import {
})
@UseGuards(PermissionsGuard)
@DocsTags("Event types")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-06-14\``,
required: true,
})
export class EventTypesController_2024_06_14 {
constructor(private readonly eventTypesService: EventTypesService_2024_06_14) {}
@Post("/")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
async createEventType(
@Body() body: CreateEventTypeInput_2024_06_14,
@GetUser() user: UserWithProfile
@@ -60,6 +71,12 @@ export class EventTypesController_2024_06_14 {
@Get("/:eventTypeId")
@Permissions([EVENT_TYPE_READ])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
async getEventTypeById(
@Param("eventTypeId") eventTypeId: string,
@GetUser() user: UserWithProfile
@@ -91,6 +108,12 @@ export class EventTypesController_2024_06_14 {
@Patch("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
@HttpCode(HttpStatus.OK)
async updateEventType(
@Param("eventTypeId") eventTypeId: number,
@@ -108,6 +131,12 @@ export class EventTypesController_2024_06_14 {
@Delete("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
async deleteEventType(
@Param("eventTypeId") eventTypeId: number,
@GetUser("id") userId: number
@@ -1,12 +1,13 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import {
getEventTypeById,
transformApiEventTypeBookingFields,
transformApiEventTypeLocations,
transformBookingFieldsApiToInternal,
transformLocationsApiToInternal,
} from "@calcom/platform-libraries";
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import type { PrismaClient } from "@calcom/prisma";
@@ -25,16 +26,21 @@ type InputEventTransformed = Omit<
| "periodCountCalendarDays"
| "periodStartDate"
| "periodEndDate"
| "recurrence"
> & {
length: number;
slug: string;
locations?: ReturnType<typeof transformApiEventTypeLocations>;
bookingFields?: ReturnType<typeof transformApiEventTypeBookingFields>;
locations?: ReturnType<typeof transformLocationsApiToInternal>;
bookingFields?: ReturnType<typeof transformBookingFieldsApiToInternal>;
};
@Injectable()
export class EventTypesRepository_2024_06_14 {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
constructor(
private readonly dbRead: PrismaReadService,
private readonly dbWrite: PrismaWriteService,
private usersService: UsersService
) {}
async createUserEventType(userId: number, body: InputEventTransformed) {
return this.dbWrite.prisma.eventType.create({
@@ -80,7 +86,7 @@ export class EventTypesRepository_2024_06_14 {
eventTypeId: number
) {
return await getEventTypeById({
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
currentOrganizationId: this.usersService.getUserMainOrgId(user),
eventTypeId,
userId: user.id,
prisma: this.dbRead.prisma as unknown as PrismaClient,
@@ -0,0 +1,71 @@
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsString, IsBoolean, IsOptional, IsEnum, Matches } from "class-validator";
export enum TemplateType {
CHECK_IN_APPOINTMENT = "CHECK_IN_APPOINTMENT",
CUSTOM_TEMPLATE = "CUSTOM_TEMPLATE",
}
export class CreatePhoneCallInput {
@IsString()
@Matches(/^\+[1-9]\d{1,14}$/, {
message:
"Invalid phone number format. Expected format: +<CountryCode><PhoneNumber> with no spaces or separators.",
})
@DocsProperty({ description: "Your phone number" })
yourPhoneNumber!: string;
@IsString()
@Matches(/^\+[1-9]\d{1,14}$/, {
message:
"Invalid phone number format. Expected format: +<CountryCode><PhoneNumber> with no spaces or separators.",
})
@DocsProperty({ description: "Number to call" })
numberToCall!: string;
@IsString()
@DocsProperty({ description: "CAL API Key" })
calApiKey!: string;
@IsBoolean()
@DocsProperty({ description: "Enabled status", default: true })
enabled = true;
@IsEnum(TemplateType)
@DocsProperty({ description: "Template type", enum: TemplateType })
templateType: TemplateType = TemplateType.CUSTOM_TEMPLATE;
@IsOptional()
@IsString()
@DocsProperty({ description: "Scheduler name" })
schedulerName?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => (value ? value : undefined))
@DocsProperty({ description: "Guest name" })
guestName?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => (value ? value : undefined))
@DocsProperty({ description: "Guest email" })
guestEmail?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => (value ? value : undefined))
@DocsProperty({ description: "Guest company" })
guestCompany?: string;
@IsOptional()
@IsString()
@DocsProperty({ description: "Begin message" })
beginMessage?: string;
@IsOptional()
@IsString()
@DocsProperty({ description: "General prompt" })
generalPrompt?: string;
}
@@ -0,0 +1,28 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsString, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Data {
@IsString()
@ApiProperty()
callId!: string;
@IsString()
@ApiProperty()
agentId!: string;
}
export class CreatePhoneCallOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: Data,
})
@ValidateNested()
@Type(() => Data)
data!: Data;
}
@@ -46,6 +46,7 @@ export class EventTypesService_2024_06_14 {
periodCountCalendarDays = undefined,
periodStartDate = undefined,
periodEndDate = undefined,
recurrence = undefined,
...bodyTransformed
} = this.inputEventTypesService.transformInputCreateEventType(body);
const { eventType: eventTypeCreated } = await createEventType({
@@ -68,6 +69,7 @@ export class EventTypesService_2024_06_14 {
periodCountCalendarDays,
periodStartDate,
periodEndDate,
recurrence,
...bodyTransformed,
},
ctx: {
@@ -119,11 +121,11 @@ export class EventTypesService_2024_06_14 {
}
async getUserToCreateEvent(user: UserWithProfile) {
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
const organizationId = this.usersService.getUserMainOrgId(user);
const isOrgAdmin = organizationId
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
: false;
const profileId = user.movedToProfile?.id || null;
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
return {
id: user.id,
@@ -159,7 +161,7 @@ export class EventTypesService_2024_06_14 {
}
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
const organizationId = this.usersService.getUserMainOrgId(user);
const isUserOrganizationAdmin = organizationId
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
@@ -270,7 +272,7 @@ export class EventTypesService_2024_06_14 {
}
async getUserToUpdateEvent(user: UserWithProfile) {
const profileId = user.movedToProfile?.id || null;
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
return { ...user, profile: { id: profileId }, selectedCalendars };
}
@@ -1,11 +1,16 @@
import { Injectable } from "@nestjs/common";
import {
transformApiEventTypeBookingFields,
transformApiEventTypeLocations,
transformApiEventTypeIntervalLimits,
transformApiEventTypeFutureBookingLimits,
transformBookingFieldsApiToInternal,
transformLocationsApiToInternal,
transformIntervalLimitsApiToInternal,
transformFutureBookingLimitsApiToInternal,
transformRecurrenceApiToInternal,
systemBeforeFieldName,
systemBeforeFieldEmail,
systemAfterFieldRescheduleReason,
} from "@calcom/platform-libraries";
import { systemBeforeFieldLocation } from "@calcom/platform-libraries";
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
@Injectable()
@@ -25,19 +30,22 @@ export class InputEventTypesService_2024_06_14 {
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
recurrence,
...rest
} = inputEventType;
const hasMultipleLocations = (locations || defaultLocations).length > 1;
const eventType = {
...rest,
length: lengthInMinutes,
locations: this.transformInputLocations(locations || defaultLocations),
bookingFields: this.transformInputBookingFields(bookingFields),
bookingFields: this.transformInputBookingFields(bookingFields, hasMultipleLocations),
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
: undefined,
...this.transformInputBookingWindow(bookingWindow),
recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined,
};
return eventType;
@@ -52,39 +60,61 @@ export class InputEventTypesService_2024_06_14 {
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
recurrence,
...rest
} = inputEventType;
const hasMultipleLocations = !!(locations && locations?.length > 1);
const eventType = {
...rest,
length: lengthInMinutes,
locations: locations ? this.transformInputLocations(locations) : undefined,
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
bookingFields: bookingFields
? this.transformInputBookingFields(bookingFields, hasMultipleLocations)
: undefined,
schedule: scheduleId,
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
: undefined,
...this.transformInputBookingWindow(bookingWindow),
recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined,
};
return eventType;
}
transformInputLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) {
return transformApiEventTypeLocations(inputLocations);
return transformLocationsApiToInternal(inputLocations);
}
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
return transformApiEventTypeBookingFields(inputBookingFields);
transformInputBookingFields(
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
hasMultipleLocations: boolean
) {
const defaultFieldsBefore = [systemBeforeFieldName, systemBeforeFieldEmail];
// note(Lauris): if event type has multiple locations then a radio button booking field has to be displayed to allow booker to pick location
if (hasMultipleLocations) {
defaultFieldsBefore.push(systemBeforeFieldLocation);
}
const customFields = transformBookingFieldsApiToInternal(inputBookingFields);
const defaultFieldsAfter = [systemAfterFieldRescheduleReason];
return [...defaultFieldsBefore, ...customFields, ...defaultFieldsAfter];
}
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
return transformApiEventTypeIntervalLimits(inputBookingFields);
return transformIntervalLimitsApiToInternal(inputBookingFields);
}
transformInputBookingWindow(inputBookingWindow: CreateEventTypeInput_2024_06_14["bookingWindow"]) {
const res = transformApiEventTypeFutureBookingLimits(inputBookingWindow);
const res = transformFutureBookingLimitsApiToInternal(inputBookingWindow);
return !!res ? res : {};
}
transformInputRecurrignEvent(recurrence: CreateEventTypeInput_2024_06_14["recurrence"]) {
return transformRecurrenceApiToInternal(recurrence);
}
}
@@ -4,16 +4,17 @@ import type { EventType, User, Schedule } from "@prisma/client";
import {
EventTypeMetaDataSchema,
userMetadata,
getResponseEventTypeLocations,
getResponseEventTypeBookingFields,
transformLocationsInternalToApi,
transformBookingFieldsInternalToApi,
parseRecurringEvent,
TransformedLocationsSchema,
BookingFieldsSchema,
SystemField,
UserField,
CustomField,
parseBookingLimit,
getResponseEventTypeIntervalLimits,
getResponseEventTypeFutureBookingLimits,
transformIntervalLimitsInternalToApi,
transformFutureBookingLimitsInternalToApi,
transformRecurrenceInternalToApi,
} from "@calcom/platform-libraries";
import { TransformFutureBookingsLimitSchema_2024_06_14 } from "@calcom/platform-types";
@@ -92,7 +93,7 @@ export class OutputEventTypesService_2024_06_14 {
const bookingFields = databaseEventType.bookingFields
? this.transformBookingFields(BookingFieldsSchema.parse(databaseEventType.bookingFields))
: [];
const recurringEvent = this.transformRecurringEvent(databaseEventType.recurringEvent);
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
const users = this.transformUsers(databaseEventType.users);
const bookingLimitsCount = this.transformIntervalLimits(databaseEventType.bookingLimits);
@@ -114,7 +115,7 @@ export class OutputEventTypesService_2024_06_14 {
description: description || "",
locations,
bookingFields,
recurringEvent,
recurrence,
disableGuests,
slotInterval,
minimumBookingNotice,
@@ -143,18 +144,20 @@ export class OutputEventTypesService_2024_06_14 {
transformLocations(locations: any) {
if (!locations) return [];
return getResponseEventTypeLocations(TransformedLocationsSchema.parse(locations));
return transformLocationsInternalToApi(TransformedLocationsSchema.parse(locations));
}
transformBookingFields(inputBookingFields: (SystemField | UserField)[] | null) {
if (!inputBookingFields) return [];
const userFields = inputBookingFields.filter((field) => field.editable === "user") as UserField[];
return getResponseEventTypeBookingFields(userFields);
transformBookingFields(bookingFields: (SystemField | CustomField)[] | null) {
if (!bookingFields) return [];
return transformBookingFieldsInternalToApi(bookingFields);
}
transformRecurringEvent(recurringEvent: any) {
if (!recurringEvent) return null;
return parseRecurringEvent(recurringEvent);
const recurringEventParsed = parseRecurringEvent(recurringEvent);
if (!recurringEventParsed) return null;
return transformRecurrenceInternalToApi(recurringEventParsed);
}
transformMetadata(metadata: any) {
@@ -180,10 +183,10 @@ export class OutputEventTypesService_2024_06_14 {
transformIntervalLimits(bookingLimits: any) {
const bookingLimitsParsed = parseBookingLimit(bookingLimits);
return getResponseEventTypeIntervalLimits(bookingLimitsParsed);
return transformIntervalLimitsInternalToApi(bookingLimitsParsed);
}
transformBookingWindow(bookingLimits: TransformFutureBookingsLimitSchema_2024_06_14) {
return getResponseEventTypeFutureBookingLimits(bookingLimits);
return transformFutureBookingLimitsInternalToApi(bookingLimits);
}
}
@@ -1,10 +1,20 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum } from "class-validator";
import { IsEnum, IsNumber, IsString } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class ProviderVerifyClientData {
@IsString()
clientId!: string;
@IsNumber()
organizationId!: number;
@IsString()
name!: string;
}
export class ProviderVerifyClientOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
data!: ProviderVerifyClientData;
}
@@ -40,6 +40,11 @@ export class CalProviderController {
return {
status: SUCCESS_STATUS,
data: {
clientId: oAuthClient.id,
organizationId: oAuthClient.organizationId,
name: oAuthClient.name,
},
};
}
@@ -23,7 +23,7 @@ import {
Patch,
UseGuards,
} from "@nestjs/common";
import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiResponse, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
import { Throttle } from "@nestjs/throttler";
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -36,7 +36,7 @@ import { CreateScheduleInput_2024_04_15 } from "../inputs/create-schedule.input"
version: VERSION_2024_04_15_VALUE,
})
@UseGuards(ApiAuthGuard, PermissionsGuard)
@DocsTags("Schedules")
@DocsExcludeController(true)
export class SchedulesController_2024_04_15 {
constructor(private readonly schedulesService: SchedulesService_2024_04_15) {}
@@ -17,7 +17,7 @@ import {
Patch,
UseGuards,
} from "@nestjs/common";
import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
import { Throttle } from "@nestjs/throttler";
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -38,11 +38,38 @@ import {
})
@UseGuards(ApiAuthGuard, PermissionsGuard)
@DocsTags("Schedules")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-06-11\``,
required: true,
})
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
export class SchedulesController_2024_06_11 {
constructor(private readonly schedulesService: SchedulesService_2024_06_11) {}
@Post("/")
@Permissions([SCHEDULE_WRITE])
@ApiOperation({
summary: "Create a schedule",
description: `
The point of creating schedules is for event types to be available at specific times.
First goal of schedules is to have a default schedule. If you are platform customer and created managed users, then it is important to note that each managed user should have a default schedule.
1. If you passed \`timeZone\` when creating managed user, then the default schedule from Monday to Friday from 9AM to 5PM will be created with that timezone. Managed user can then change the default schedule via \`AvailabilitySettings\` atom.
2. If you did not, then we assume you want that user has specific schedule right away. You should create default schedule by specifying
\`"isDefault": true\` in the request body. Until the user has a default schedule that user can't be booked or manage his / her schedule via the AvailabilitySettings atom.
Second goal is to create other schedules that event types can point to, so that when that event is booked availability is not checked against the default schedule but against that specific schedule.
After creating a non default schedule you can update event type to point to that schedule via the PATCH \`event-types/{eventTypeId}\` endpoint.
When specifying start time and end time for each day use 24 hour format e.g. 08:00, 15:00 etc.
`,
})
async createSchedule(
@GetUser() user: UserWithProfile,
@Body() bodySchedule: CreateScheduleInput_2024_06_11
@@ -59,7 +86,7 @@ export class SchedulesController_2024_06_11 {
@Permissions([SCHEDULE_READ])
@ApiResponse({
status: 200,
description: "Returns the default schedule",
description: "Returns the default schedule of the authenticated user",
type: GetDefaultScheduleOutput_2024_06_11,
})
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetScheduleOutput_2024_06_11> {
@@ -88,6 +115,9 @@ export class SchedulesController_2024_06_11 {
@Get("/")
@Permissions([SCHEDULE_READ])
@ApiOperation({
description: "Returns all schedules of the authenticated user",
})
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_06_11> {
const schedules = await this.schedulesService.getUserSchedules(user.id);
@@ -0,0 +1,6 @@
export enum AuthMethods {
"API_KEY" = "api-key",
"ACCESS_TOKEN" = "access-token",
"OAUTH_CLIENT" = "oauth-client",
"NEXT_AUTH" = "next-auth",
}
+45
View File
@@ -0,0 +1,45 @@
import { isApiKey } from "@/lib/api-key";
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Reflector } from "@nestjs/core";
import { ThrottlerGuard, ThrottlerModuleOptions, ThrottlerStorage } from "@nestjs/throttler";
import { Request } from "express";
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
private logger = new Logger("CustomThrottlerGuard");
constructor(
options: ThrottlerModuleOptions,
storageService: ThrottlerStorage,
reflector: Reflector,
private readonly config: ConfigService
) {
super(options, storageService, reflector);
}
protected async getTracker(request: Request): Promise<string> {
const authorizationHeader = request.get("Authorization")?.replace("Bearer ", "");
if (authorizationHeader) {
return isApiKey(authorizationHeader, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
? `api_key_${authorizationHeader}`
: `access_token_${authorizationHeader}`;
}
const oauthClientId = request.get(X_CAL_CLIENT_ID);
if (oauthClientId) {
return oauthClientId;
}
if (request.ip) {
return request.ip;
}
this.logger.log(`no tracker found: ${request.url}`);
return "unknown";
}
}
@@ -0,0 +1,13 @@
import { AuthMethods } from "@/lib/enums/auth-methods";
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const GetAuthMethod = createParamDecorator<unknown, ExecutionContext>((_data, ctx) => {
const request = ctx.switchToHttp().getRequest();
const authMethod = request.authMethod as AuthMethods;
if (!authMethod) {
throw new Error("GetAuthMethod decorator : auth method not set");
}
return authMethod;
});
@@ -1,6 +1,7 @@
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
@@ -12,13 +13,14 @@ export class OrganizationRolesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private organizationsService: OrganizationsService,
private membershipRepository: MembershipsRepository
private membershipRepository: MembershipsRepository,
private usersService: UsersService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user: UserWithProfile = request.user;
const organizationId = user?.movedToProfile?.organizationId || user?.organizationId;
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
if (!user || !organizationId) {
throw new ForbiddenException("No organization associated with the user.");
@@ -0,0 +1,67 @@
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository";
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Request } from "express";
import { Team } from "@calcom/prisma/client";
type CachedData = {
org?: Team;
canAccess?: boolean;
};
@Injectable()
export class IsWebhookInOrg implements CanActivate {
constructor(
private organizationsRepository: OrganizationsRepository,
private organizationsWebhooksRepository: OrganizationsWebhooksRepository,
private readonly redisService: RedisService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
let canAccess = false;
const request = context.switchToHttp().getRequest<Request & { organization: Team }>();
const webhookId: string = request.params.webhookId;
const organizationId: string = request.params.orgId;
if (!organizationId) {
throw new ForbiddenException("No organization id found in request params.");
}
if (!webhookId) {
throw new ForbiddenException("No webhook id found in request params.");
}
const REDIS_CACHE_KEY = `apiv2:org:${webhookId}:guard:isWebhookInOrg`;
const cachedData = await this.redisService.redis.get(REDIS_CACHE_KEY);
if (cachedData) {
const { org: cachedOrg, canAccess: cachedCanAccess } = JSON.parse(cachedData) as CachedData;
if (cachedOrg?.id === Number(organizationId) && cachedCanAccess !== undefined) {
request.organization = cachedOrg;
return cachedCanAccess;
}
}
const org = await this.organizationsRepository.findById(Number(organizationId));
if (org?.isOrganization) {
const isWebhookInOrg = await this.organizationsWebhooksRepository.findWebhook(
Number(organizationId),
webhookId
);
if (isWebhookInOrg) canAccess = true;
}
if (org) {
await this.redisService.redis.set(
REDIS_CACHE_KEY,
JSON.stringify({ org: org, canAccess } satisfies CachedData),
"EX",
300
);
}
return canAccess;
}
}
@@ -1,4 +1,5 @@
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { AuthMethods } from "@/lib/enums/auth-methods";
import { BaseStrategy } from "@/lib/passport/strategies/types";
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
import { DeploymentsService } from "@/modules/deployments/deployments.service";
@@ -30,7 +31,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
super();
}
async authenticate(request: Request) {
async authenticate(request: Request & { authMethod: AuthMethods }) {
try {
const { params } = request;
const oAuthClientSecret = request.get(X_CAL_SECRET_KEY);
@@ -38,11 +39,15 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
const bearerToken = request.get("Authorization")?.replace("Bearer ", "");
if (oAuthClientId && oAuthClientSecret) {
request.authMethod = AuthMethods["OAUTH_CLIENT"];
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret);
}
if (bearerToken) {
const requestOrigin = request.get("Origin");
request.authMethod = isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
? AuthMethods["API_KEY"]
: AuthMethods["ACCESS_TOKEN"];
return await this.authenticateBearerToken(bearerToken, requestOrigin);
}
@@ -50,6 +55,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
if (nextAuthToken) {
request.authMethod = AuthMethods["NEXT_AUTH"];
return await this.authenticateNextAuth(nextAuthToken);
}
@@ -7,6 +7,7 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { UsersModule } from "@/modules/users/users.module";
import { BullModule } from "@nestjs/bull";
import { Module } from "@nestjs/common";
@@ -23,6 +24,7 @@ import { Module } from "@nestjs/common";
duration: 1000,
},
}),
UsersModule,
],
providers: [BillingConfigService, BillingService, BillingRepository, BillingProcessor],
exports: [BillingService, BillingRepository],
@@ -9,12 +9,12 @@ import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
export class CredentialsRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async createAppCredential(
async upsertAppCredential(
type: keyof typeof APPS_TYPE_ID_MAPPING,
key: Prisma.InputJsonValue,
userId: number
userId: number,
credentialId?: number | null
) {
const credential = await this.getByTypeAndUserId(type, userId);
return this.dbWrite.prisma.credential.upsert({
create: {
type,
@@ -27,7 +27,7 @@ export class CredentialsRepository {
invalid: false,
},
where: {
id: credential?.id ?? 0,
id: credentialId ?? 0,
},
});
}
@@ -36,6 +36,10 @@ export class CredentialsRepository {
return this.dbWrite.prisma.credential.findFirst({ where: { type, userId } });
}
getAllUserCredentialsByTypeAndId(type: string, userId: number) {
return this.dbRead.prisma.credential.findMany({ where: { type, userId } });
}
getUserCredentialsByIds(userId: number, credentialIds: number[]) {
return this.dbRead.prisma.credential.findMany({
where: {
@@ -60,6 +64,30 @@ export class CredentialsRepository {
},
});
}
async getUserCredentialById(userId: number, credentialId: number, type: string) {
return await this.dbRead.prisma.credential.findUnique({
where: {
userId,
type,
id: credentialId,
},
select: {
id: true,
type: true,
key: true,
userId: true,
teamId: true,
appId: true,
invalid: true,
user: {
select: {
email: true,
},
},
},
});
}
}
export type CredentialsWithUserEmail = Awaited<
@@ -33,7 +33,7 @@ export class DeploymentsService {
if (!licenseKey) {
return false;
}
const licenseKeyUrl = this.configService.get("api.licenseKeyUrl") + `?key=${licenseKey}`;
const licenseKeyUrl = this.configService.get("api.licenseKeyUrl") + `/${licenseKey}`;
const cachedData = await this.redisService.redis.get(getLicenseCacheKey(licenseKey));
if (cachedData) {
return (JSON.parse(cachedData) as LicenseCheckResponse)?.valid;
@@ -6,6 +6,7 @@ import { DestinationCalendarsController } from "@/modules/destination-calendars/
import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository";
import { DestinationCalendarsService } from "@/modules/destination-calendars/services/destination-calendars.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@@ -19,6 +20,7 @@ import { Module } from "@nestjs/common";
UsersRepository,
CredentialsRepository,
AppsRepository,
SelectedCalendarsRepository,
],
controllers: [DestinationCalendarsController],
exports: [DestinationCalendarsRepository],
@@ -79,7 +79,6 @@ export class OAuthClientUsersController {
`Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}`
);
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
console.log("asap createUser client", JSON.stringify(client, null, 2));
const isPlatformManaged = true;
const { user, tokens } = await this.oAuthClientUsersService.createOauthClientUser(
@@ -13,6 +13,7 @@ import { OAuthClientGuard } from "@/modules/oauth-clients/guards/oauth-client-gu
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import {
@@ -59,7 +60,8 @@ export class OAuthClientsController {
constructor(
private readonly oauthClientRepository: OAuthClientRepository,
private readonly userRepository: UsersRepository,
private readonly teamsRepository: OrganizationsRepository
private readonly teamsRepository: OrganizationsRepository,
private usersService: UsersService
) {}
@Post("/")
@@ -74,7 +76,7 @@ export class OAuthClientsController {
@GetUser() user: UserWithProfile,
@Body() body: CreateOAuthClientInput
): Promise<CreateOAuthClientResponseDto> {
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
const organizationId = this.usersService.getUserMainOrgId(user) as number;
this.logger.log(
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
);
@@ -100,7 +102,7 @@ export class OAuthClientsController {
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
@DocsOperation({ description: AUTH_DOCUMENTATION })
async getOAuthClients(@GetUser() user: UserWithProfile): Promise<GetOAuthClientsResponseDto> {
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
const organizationId = this.usersService.getUserMainOrgId(user) as number;
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
return { status: SUCCESS_STATUS, data: clients };
@@ -1,5 +1,6 @@
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { UsersService } from "@/modules/users/services/users.service";
import {
Injectable,
CanActivate,
@@ -11,12 +12,12 @@ import { Request } from "express";
@Injectable()
export class OAuthClientGuard implements CanActivate {
constructor(private oAuthClientRepository: OAuthClientRepository) {}
constructor(private oAuthClientRepository: OAuthClientRepository, private usersService: UsersService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request & { user: GetUserReturnType }>();
const user: GetUserReturnType = request.user;
const organizationId = user?.movedToProfile?.organizationId || user?.organizationId;
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
const oAuthClientId = request.params.clientId;
if (!oAuthClientId) {
@@ -0,0 +1,250 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/inputs/attributes/assign/organizations-attributes-options-assign.input";
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/update-organizaiton-attribute-option.input.ts";
import { PrismaModule } from "@/modules/prisma/prisma.module";
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 * as request from "supertest";
import { AttributeRepositoryFixture } from "test/fixtures/repository/attributes.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { User, Team, Membership } from "@calcom/prisma/client";
describe("Organizations Attributes Options Endpoints", () => {
describe("User lacks required role", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
const userEmail = "member@attributes-options-api.com";
let user: User;
let org: Team;
let membership: Membership;
let attributeId: string;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "AttributesOptionsCorp",
isOrganization: true,
});
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
organization: { connect: { id: org.id } },
});
membership = await membershipFixtures.addUserToOrg(user, org, "MEMBER", true);
// Create an attribute for testing
attributeId = "test-attribute-id";
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should not be able to create attribute option", async () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/attributes/${attributeId}/options`)
.send({
name: "Option 1",
value: "option1",
})
.expect(403);
});
it("should not be able to delete attribute option", async () => {
return request(app.getHttpServer())
.delete(`/v2/organizations/${org.id}/attributes/${attributeId}/options/1`)
.expect(403);
});
afterAll(async () => {
await membershipFixtures.delete(membership.id);
await userRepositoryFixture.deleteByEmail(user.email);
await organizationsRepositoryFixture.delete(org.id);
await app.close();
});
});
describe("User has required role", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
let attributeRepositoryFixture: AttributeRepositoryFixture;
const userEmail = "admin@attributes-options-api.com";
let user: User;
let org: Team;
let membership: Membership;
let attributeId: string;
let createdOption: any;
const createOptionInput: CreateOrganizationAttributeOptionInput = {
value: "option1",
slug: "option1",
};
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
attributeRepositoryFixture = new AttributeRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "AttributesOptionsCorp",
isOrganization: true,
});
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
organization: { connect: { id: org.id } },
});
membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true);
// Create an attribute for testing
const attribute = await attributeRepositoryFixture.create({
name: "Test Attribute",
team: { connect: { id: org.id } },
type: "TEXT",
slug: "test-attribute",
});
attributeId = attribute.id;
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should create attribute option", async () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/attributes/${attributeId}/options`)
.send(createOptionInput)
.expect(201)
.then((response) => {
expect(response.body.status).toEqual(SUCCESS_STATUS);
createdOption = response.body.data;
expect(createdOption.value).toEqual(createOptionInput.value);
expect(createdOption.slug).toEqual(createOptionInput.slug);
});
});
it("should get attribute options", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/attributes/${attributeId}/options`)
.expect(200)
.then((response) => {
expect(response.body.status).toEqual(SUCCESS_STATUS);
const options = response.body.data;
expect(options.length).toEqual(1);
expect(options[0].value).toEqual(createOptionInput.value);
expect(options[0].slug).toEqual(createOptionInput.slug);
});
});
it("should update attribute option", async () => {
const updateOptionInput: UpdateOrganizationAttributeOptionInput = {
value: "updated-option-value",
};
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/attributes/${attributeId}/options/${createdOption.id}`)
.send(updateOptionInput)
.expect(200)
.then((response) => {
expect(response.body.status).toEqual(SUCCESS_STATUS);
const updatedOption = response.body.data;
expect(updatedOption.value).toEqual(updateOptionInput.value);
});
});
it("should assign attribute option to user", async () => {
const assignInput: AssignOrganizationAttributeOptionToUserInput = {
attributeId: attributeId,
attributeOptionId: createdOption.id,
};
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/attributes/options/${user.id}`)
.send(assignInput)
.expect(201)
.then((response) => {
expect(response.body.status).toEqual(SUCCESS_STATUS);
expect(response.body.data).toBeTruthy();
});
});
it("should get attribute options for user", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/attributes/options/${user.id}`)
.expect(200)
.then((response) => {
expect(response.body.status).toEqual(SUCCESS_STATUS);
const userOptions = response.body.data;
expect(userOptions.length).toEqual(1);
expect(userOptions[0].id).toEqual(createdOption.id);
});
});
it("should unassign attribute option from user", async () => {
return request(app.getHttpServer())
.delete(`/v2/organizations/${org.id}/attributes/options/${user.id}/${createdOption.id}`)
.expect(200)
.then((response) => {
expect(response.body.status).toEqual(SUCCESS_STATUS);
expect(response.body.data).toBeTruthy();
});
});
it("should delete attribute option", async () => {
return request(app.getHttpServer())
.delete(`/v2/organizations/${org.id}/attributes/${attributeId}/options/${createdOption.id}`)
.expect(200)
.then((response) => {
expect(response.body.status).toEqual(SUCCESS_STATUS);
expect(response.body.data).toBeTruthy();
});
});
afterAll(async () => {
await membershipFixtures.delete(membership.id);
await userRepositoryFixture.deleteByEmail(user.email);
await organizationsRepositoryFixture.delete(org.id);
await app.close();
});
});
});
@@ -0,0 +1,213 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/create-organization-attribute.input";
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/update-organization-attribute.input";
import { PrismaModule } from "@/modules/prisma/prisma.module";
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 * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { User, Team, Membership } from "@calcom/prisma/client";
describe("Organizations Attributes Endpoints", () => {
describe("User lacks required role", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
const userEmail = "member@attributes-api.com";
let user: User;
let org: Team;
let membership: Membership;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "AttributesCorp",
isOrganization: true,
});
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
organization: { connect: { id: org.id } },
});
membership = await membershipFixtures.addUserToOrg(user, org, "MEMBER", true);
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should not be able to create attribute for org", async () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/attributes`)
.send({
key: "department",
value: "engineering",
})
.expect(403);
});
it("should not be able to delete attribute for org", async () => {
return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/attributes/1`).expect(403);
});
afterAll(async () => {
await membershipFixtures.delete(membership.id);
await userRepositoryFixture.deleteByEmail(user.email);
await organizationsRepositoryFixture.delete(org.id);
await app.close();
});
});
describe("User has required role", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
const userEmail = "admin@attributes-api.com";
let user: User;
let org: Team;
let membership: Membership;
let createdAttribute: any;
const createAttributeInput: CreateOrganizationAttributeInput = {
name: "department",
slug: "department",
type: "TEXT",
options: [],
enabled: true,
};
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "AttributesCorp",
isOrganization: true,
});
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
organization: { connect: { id: org.id } },
});
membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true);
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should create attribute for org", async () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/attributes`)
.send(createAttributeInput)
.expect(201)
.then((response) => {
expect(response.body.status).toEqual(SUCCESS_STATUS);
createdAttribute = response.body.data;
expect(createdAttribute.type).toEqual(createAttributeInput.type);
expect(createdAttribute.slug).toEqual(createAttributeInput.slug);
expect(createdAttribute.enabled).toEqual(createAttributeInput.enabled);
});
});
it("should get org attributes", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/attributes`)
.expect(200)
.then((response) => {
expect(response.body.status).toEqual(SUCCESS_STATUS);
const attributes = response.body.data;
expect(attributes.length).toEqual(1);
expect(attributes[0].name).toEqual(createAttributeInput.name);
expect(attributes[0].slug).toEqual(createAttributeInput.slug);
expect(attributes[0].type).toEqual(createAttributeInput.type);
expect(attributes[0].enabled).toEqual(createAttributeInput.enabled);
});
});
it("should get single org attribute", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/attributes/${createdAttribute.id}`)
.expect(200)
.then((response) => {
expect(response.body.status).toEqual(SUCCESS_STATUS);
const attribute = response.body.data;
expect(attribute.name).toEqual(createAttributeInput.name);
expect(attribute.slug).toEqual(createAttributeInput.slug);
expect(attribute.type).toEqual(createAttributeInput.type);
expect(attribute.enabled).toEqual(createAttributeInput.enabled);
});
});
it("should update org attribute", async () => {
const updateAttributeInput: UpdateOrganizationAttributeInput = {
name: "marketing",
};
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/attributes/${createdAttribute.id}`)
.send(updateAttributeInput)
.expect(200)
.then((response) => {
expect(response.body.status).toEqual(SUCCESS_STATUS);
const updatedAttribute = response.body.data;
expect(updatedAttribute.name).toEqual(updateAttributeInput.name);
});
});
it("should delete org attribute", async () => {
return request(app.getHttpServer())
.delete(`/v2/organizations/${org.id}/attributes/${createdAttribute.id}`)
.expect(200)
.then((response) => {
expect(response.body.status).toEqual(SUCCESS_STATUS);
expect(response.body.data).toBeTruthy();
});
});
afterAll(async () => {
await membershipFixtures.delete(membership.id);
await userRepositoryFixture.deleteByEmail(user.email);
await organizationsRepositoryFixture.delete(org.id);
await app.close();
});
});
});
@@ -0,0 +1,173 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/inputs/attributes/assign/organizations-attributes-options-assign.input";
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/update-organizaiton-attribute-option.input.ts";
import {
AssignOptionUserOutput,
UnassignOptionUserOutput,
} from "@/modules/organizations/outputs/attributes/options/assign-option-user.output";
import { CreateAttributeOptionOutput } from "@/modules/organizations/outputs/attributes/options/create-option.output";
import { DeleteAttributeOptionOutput } from "@/modules/organizations/outputs/attributes/options/delete-option.output";
import { GetOptionUserOutput } from "@/modules/organizations/outputs/attributes/options/get-option-user.output";
import { GetAllAttributeOptionOutput } from "@/modules/organizations/outputs/attributes/options/get-option.output";
import { UpdateAttributeOptionOutput } from "@/modules/organizations/outputs/attributes/options/update-option.output";
import { OrganizationAttributeOptionService } from "@/modules/organizations/services/attributes/organization-attributes-option.service";
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@Controller({
path: "/v2/organizations/:orgId",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
export class OrganizationsOptionsAttributesController {
constructor(private readonly organizationsAttributesOptionsService: OrganizationAttributeOptionService) {}
// Creates an attribute option for an attribute
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@Post("/attributes/:attributeId/options")
async createOrganizationAttributeOption(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("attributeId") attributeId: string,
@Body() bodyAttribute: CreateOrganizationAttributeOptionInput
): Promise<CreateAttributeOptionOutput> {
const attributeOption =
await this.organizationsAttributesOptionsService.createOrganizationAttributeOption(
orgId,
attributeId,
bodyAttribute
);
return {
status: SUCCESS_STATUS,
data: attributeOption,
};
}
// Deletes an attribute option for an attribute
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@Delete("/attributes/:attributeId/options/:optionId")
async deleteOrganizationAttributeOption(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("attributeId") attributeId: string,
@Param("optionId") optionId: string
): Promise<DeleteAttributeOptionOutput> {
const attributeOption =
await this.organizationsAttributesOptionsService.deleteOrganizationAttributeOption(
orgId,
attributeId,
optionId
);
return {
status: SUCCESS_STATUS,
data: attributeOption,
};
}
// Updates an attribute option for an attribute
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@Patch("/attributes/:attributeId/options/:optionId")
async updateOrganizationAttributeOption(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("attributeId") attributeId: string,
@Param("optionId") optionId: string,
@Body() bodyAttribute: UpdateOrganizationAttributeOptionInput
): Promise<UpdateAttributeOptionOutput> {
const attributeOption =
await this.organizationsAttributesOptionsService.updateOrganizationAttributeOption(
orgId,
attributeId,
optionId,
bodyAttribute
);
return {
status: SUCCESS_STATUS,
data: attributeOption,
};
}
// Gets all attribute options for an attribute
@Roles("ORG_MEMBER")
@PlatformPlan("ESSENTIALS")
@Get("/attributes/:attributeId/options")
async getOrganizationAttributeOptions(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("attributeId") attributeId: string
): Promise<GetAllAttributeOptionOutput> {
const attributeOptions = await this.organizationsAttributesOptionsService.getOrganizationAttributeOptions(
orgId,
attributeId
);
return {
status: SUCCESS_STATUS,
data: attributeOptions,
};
}
// Assign attribute option to user
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@Post("/attributes/options/:userId")
async assignOrganizationAttributeOptionToUser(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("userId", ParseIntPipe) userId: number,
@Body() bodyAttribute: AssignOrganizationAttributeOptionToUserInput
): Promise<AssignOptionUserOutput> {
const attributeOption =
await this.organizationsAttributesOptionsService.assignOrganizationAttributeOptionToUser(
orgId,
userId,
bodyAttribute
);
return {
status: SUCCESS_STATUS,
data: attributeOption,
};
}
// Unassign attribute option from user
@Roles("ORG_MEMBER")
@PlatformPlan("ESSENTIALS")
@Delete("/attributes/options/:userId/:attributeOptionId")
async unassignOrganizationAttributeOptionFromUser(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("userId", ParseIntPipe) userId: number,
@Param("attributeOptionId") attributeOptionId: string
): Promise<UnassignOptionUserOutput> {
const attributeOption =
await this.organizationsAttributesOptionsService.unassignOrganizationAttributeOptionFromUser(
orgId,
userId,
attributeOptionId
);
return {
status: SUCCESS_STATUS,
data: attributeOption,
};
}
@Roles("ORG_MEMBER")
@PlatformPlan("ESSENTIALS")
@Get("/attributes/options/:userId")
async getOrganizationAttributeOptionsForUser(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("userId", ParseIntPipe) userId: number
): Promise<GetOptionUserOutput> {
const attributeOptions =
await this.organizationsAttributesOptionsService.getOrganizationAttributeOptionsForUser(orgId, userId);
return {
status: SUCCESS_STATUS,
data: attributeOptions,
};
}
}
@@ -0,0 +1,129 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { CreateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/create-organization-attribute.input";
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/update-organization-attribute.input";
import { CreateOrganizationAttributesOutput } from "@/modules/organizations/outputs/attributes/create-organization-attributes.output";
import { DeleteOrganizationAttributesOutput } from "@/modules/organizations/outputs/attributes/delete-organization-attributes.output";
import {
GetOrganizationAttributesOutput,
GetSingleAttributeOutput,
} from "@/modules/organizations/outputs/attributes/get-organization-attributes.output";
import { UpdateOrganizationAttributesOutput } from "@/modules/organizations/outputs/attributes/update-organization-attributes.output";
import { OrganizationAttributesService } from "@/modules/organizations/services/attributes/organization-attributes.service";
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
Query,
UseGuards,
} from "@nestjs/common";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { SkipTakePagination } from "@calcom/platform-types";
@Controller({
path: "/v2/organizations/:orgId",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
export class OrganizationsAttributesController {
constructor(private readonly organizationsAttributesService: OrganizationAttributesService) {}
// Gets all attributes for an organization
@Roles("ORG_MEMBER")
@PlatformPlan("ESSENTIALS")
@Get("/attributes")
async getOrganizationAttributes(
@Param("orgId", ParseIntPipe) orgId: number,
@Query() queryParams: SkipTakePagination
): Promise<GetOrganizationAttributesOutput> {
const { skip, take } = queryParams;
const attributes = await this.organizationsAttributesService.getOrganizationAttributes(orgId, skip, take);
return {
status: SUCCESS_STATUS,
data: attributes,
};
}
// Gets a single attribute for an organization
@Roles("ORG_MEMBER")
@PlatformPlan("ESSENTIALS")
@Get("/attributes/:attributeId")
async getOrganizationAttribute(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("attributeId") attributeId: string
): Promise<GetSingleAttributeOutput> {
const attribute = await this.organizationsAttributesService.getOrganizationAttribute(orgId, attributeId);
return {
status: SUCCESS_STATUS,
data: attribute,
};
}
// Creates an attribute for an organization
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@Post("/attributes")
async createOrganizationAttribute(
@Param("orgId", ParseIntPipe) orgId: number,
@Body() bodyAttribute: CreateOrganizationAttributeInput
): Promise<CreateOrganizationAttributesOutput> {
const attribute = await this.organizationsAttributesService.createOrganizationAttribute(
orgId,
bodyAttribute
);
return {
status: SUCCESS_STATUS,
data: attribute,
};
}
// Updates an attribute for an organization
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@Patch("/attributes/:attributeId")
async updateOrganizationAttribute(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("attributeId") attributeId: string,
@Body() bodyAttribute: UpdateOrganizationAttributeInput
): Promise<UpdateOrganizationAttributesOutput> {
const attribute = await this.organizationsAttributesService.updateOrganizationAttribute(
orgId,
attributeId,
bodyAttribute
);
return {
status: SUCCESS_STATUS,
data: attribute,
};
}
// Deletes an attribute for an organization
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@Delete("/attributes/:attributeId")
async deleteOrganizationAttribute(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("attributeId") attributeId: string
): Promise<DeleteOrganizationAttributesOutput> {
const attribute = await this.organizationsAttributesService.deleteOrganizationAttribute(
orgId,
attributeId
);
return {
status: SUCCESS_STATUS,
data: attribute,
};
}
}
@@ -1,3 +1,5 @@
import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input";
import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
@@ -33,6 +35,7 @@ import {
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { handleCreatePhoneCall } from "@calcom/platform-libraries";
import {
CreateTeamEventTypeInput_2024_06_14,
GetTeamEventTypesQuery_2024_06_14,
@@ -91,6 +94,30 @@ export class OrganizationsEventTypesController {
};
}
@Roles("TEAM_ADMIN")
@Post("/teams/:teamId/event-types/:eventTypeId/create-phone-call")
@UseGuards(ApiAuthGuard, IsOrgGuard, IsTeamInOrg, RolesGuard)
async createPhoneCall(
@Param("eventTypeId") eventTypeId: number,
@Param("orgId", ParseIntPipe) orgId: number,
@Body() body: CreatePhoneCallInput,
@GetUser() user: UserWithProfile
): Promise<CreatePhoneCallOutput> {
const data = await handleCreatePhoneCall({
user: {
id: user.id,
timeZone: user.timeZone,
profile: { organization: { id: orgId } },
},
input: { ...body, eventTypeId },
});
return {
status: SUCCESS_STATUS,
data,
};
}
@UseGuards(IsOrgGuard, IsTeamInOrg, IsAdminAPIEnabledGuard)
@Get("/teams/:teamId/event-types")
async getTeamEventTypes(
@@ -138,7 +165,7 @@ export class OrganizationsEventTypesController {
@Patch("/teams/:teamId/event-types/:eventTypeId")
async updateTeamEventType(
@Param("teamId", ParseIntPipe) teamId: number,
@Param("eventTypeId") eventTypeId: number,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@GetUser() user: UserWithProfile,
@Body() bodyEventType: UpdateTeamEventTypeInput_2024_06_14
): Promise<UpdateTeamEventTypeOutput> {
@@ -17,6 +17,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-enums";
import {
ApiSuccessResponse,
CreateTeamEventTypeInput_2024_06_14,
@@ -24,7 +25,6 @@ import {
TeamEventTypeOutput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/enums/booking-window.enum";
import { Team } from "@calcom/prisma/client";
describe("Organizations Event Types Endpoints", () => {
@@ -1,6 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetMembership } from "@/modules/auth/decorators/get-membership/get-membership.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
@@ -36,7 +35,6 @@ import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { SkipTakePagination } from "@calcom/platform-types";
import { Membership } from "@calcom/prisma/client";
@Controller({
path: "/v2/organizations/:orgId/memberships",
@@ -89,7 +87,11 @@ export class OrganizationsMembershipsController {
@UseGuards(IsMembershipInOrg)
@Get("/:membershipId")
@HttpCode(HttpStatus.OK)
async getUserSchedule(@GetMembership() membership: Membership): Promise<GetOrgMembership> {
async getOrgMembership(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("membershipId", ParseIntPipe) membershipId: number
): Promise<GetOrgMembership> {
const membership = await this.organizationsMembershipService.getOrgMembership(orgId, membershipId);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgMembershipOutputDto, membership, { strategy: "excludeAll" }),
@@ -0,0 +1,147 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { IsWebhookInOrg } from "@/modules/auth/guards/organizations/is-webhook-in-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { OrganizationsWebhooksService } from "@/modules/organizations/services/organizations-webhooks.service";
import { CreateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
import { UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
import {
TeamWebhookOutputDto as OrgWebhookOutputDto,
TeamWebhookOutputResponseDto as OrgWebhookOutputResponseDto,
TeamWebhooksOutputResponseDto as OrgWebhooksOutputResponseDto,
} from "@/modules/webhooks/outputs/team-webhook.output";
import { PartialWebhookInputPipe, WebhookInputPipe } from "@/modules/webhooks/pipes/WebhookInputPipe";
import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
import {
Controller,
UseGuards,
Get,
Param,
ParseIntPipe,
Query,
Delete,
Patch,
Post,
Body,
HttpCode,
HttpStatus,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { SkipTakePagination } from "@calcom/platform-types";
@Controller({
path: "/v2/organizations/:orgId/webhooks",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Organizations Webhooks")
export class OrganizationsWebhooksController {
constructor(
private organizationsWebhooksService: OrganizationsWebhooksService,
private webhooksService: WebhooksService
) {}
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@Get("/")
@HttpCode(HttpStatus.OK)
async getAllOrganizationWebhooks(
@Param("orgId", ParseIntPipe) orgId: number,
@Query() queryParams: SkipTakePagination
): Promise<OrgWebhooksOutputResponseDto> {
const { skip, take } = queryParams;
const webhooks = await this.organizationsWebhooksService.getWebhooksPaginated(
orgId,
skip ?? 0,
take ?? 250
);
return {
status: SUCCESS_STATUS,
data: webhooks.map((webhook) =>
plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
})
),
};
}
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@Post("/")
@HttpCode(HttpStatus.CREATED)
async createOrganizationWebhook(
@Param("orgId", ParseIntPipe) orgId: number,
@Body() body: CreateWebhookInputDto
): Promise<OrgWebhookOutputResponseDto> {
const webhook = await this.organizationsWebhooksService.createWebhook(
orgId,
new WebhookInputPipe().transform(body)
);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
}),
};
}
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsWebhookInOrg)
@Get("/:webhookId")
@HttpCode(HttpStatus.OK)
async getOrganizationWebhook(@Param("webhookId") webhookId: string): Promise<OrgWebhookOutputResponseDto> {
const webhook = await this.organizationsWebhooksService.getWebhook(webhookId);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
}),
};
}
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsWebhookInOrg)
@Delete("/:webhookId")
@HttpCode(HttpStatus.OK)
async deleteWebhook(@Param("webhookId") webhookId: string): Promise<OrgWebhookOutputResponseDto> {
const webhook = await this.webhooksService.deleteWebhook(webhookId);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
}),
};
}
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsWebhookInOrg)
@Patch("/:webhookId")
@HttpCode(HttpStatus.OK)
async updateOrgWebhook(
@Param("webhookId") webhookId: string,
@Body() body: UpdateWebhookInputDto
): Promise<OrgWebhookOutputResponseDto> {
const webhook = await this.organizationsWebhooksService.updateWebhook(
webhookId,
new PartialWebhookInputPipe().transform(body)
);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
}),
};
}
}
@@ -0,0 +1,211 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { UserWithProfile } from "@/modules/users/users.repository";
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
import {
TeamWebhookOutputResponseDto,
TeamWebhooksOutputResponseDto,
} from "@/modules/webhooks/outputs/team-webhook.output";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { Team, Webhook } from "@calcom/prisma/client";
describe("WebhooksController (e2e)", () => {
let app: INestApplication;
const userEmail = "webhook-controller-e2e@api.com";
let org: Team;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let webhookRepositoryFixture: WebhookRepositoryFixture;
let userRepositoryFixture: UserRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let webhook: TeamWebhookOutputResponseDto["data"];
let otherWebhook: Webhook;
let user: UserWithProfile;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
webhookRepositoryFixture = new WebhookRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
});
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
isOrganization: true,
});
await membershipsRepositoryFixture.create({
role: "ADMIN",
user: { connect: { id: user.id } },
team: { connect: { id: org.id } },
});
otherWebhook = await webhookRepositoryFixture.create({
id: "2mdfnn2",
subscriberUrl: "https://example.com",
eventTriggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
active: true,
payloadTemplate: "string",
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
afterAll(async () => {
userRepositoryFixture.deleteByEmail(user.email);
webhookRepositoryFixture.delete(otherWebhook.id);
await app.close();
});
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
expect(org).toBeDefined();
});
it("/organizations/:orgId/webhooks (POST)", () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/webhooks`)
.send({
subscriberUrl: "https://example.com",
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
active: true,
payloadTemplate: "string",
} satisfies CreateWebhookInputDto)
.expect(201)
.then(async (res) => {
process.stdout.write(JSON.stringify(res.body));
expect(res.body).toMatchObject({
status: "success",
data: {
id: expect.any(String),
subscriberUrl: "https://example.com",
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
active: true,
payloadTemplate: "string",
teamId: org.id,
},
} satisfies TeamWebhookOutputResponseDto);
webhook = res.body.data;
});
});
it("/organizations/:orgId/webhooks (POST) should fail to create a webhook that already has same orgId / subcriberUrl combo", () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/webhooks`)
.send({
subscriberUrl: "https://example.com",
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
active: true,
payloadTemplate: "string",
} satisfies CreateWebhookInputDto)
.expect(409);
});
it("/organizations/:orgId/webhooks/:webhookId (PATCH)", () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/webhooks/${webhook.id}`)
.send({
active: false,
} satisfies UpdateWebhookInputDto)
.expect(200)
.then((res) => {
expect(res.body.data.active).toBe(false);
});
});
it("/organizations/:orgId/webhooks/:webhookId (GET)", () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/webhooks/${webhook.id}`)
.expect(200)
.then((res) => {
expect(res.body).toMatchObject({
status: "success",
data: {
id: expect.any(String),
subscriberUrl: "https://example.com",
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
active: false,
payloadTemplate: "string",
teamId: org.id,
},
} satisfies TeamWebhookOutputResponseDto);
});
});
it("/organizations/:orgId/webhooks/:webhookId (GET) should say forbidden to get a webhook that does not exist", () => {
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/webhooks/90284`).expect(403);
});
it("/organizations/:orgId/webhooks/:webhookId (GET) should fail to get a webhook that does not belong to org", () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/webhooks/${otherWebhook.id}`)
.expect(403);
});
it("/organizations/:orgId/webhooks (GET)", () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/webhooks`)
.expect(200)
.then((res) => {
const responseBody = res.body as TeamWebhooksOutputResponseDto;
responseBody.data.forEach((webhook) => {
expect(webhook.teamId).toBe(org.id);
});
});
});
it("/organizations/:orgId/webhooks/:webhookId (DELETE)", () => {
return request(app.getHttpServer())
.delete(`/v2/organizations/${org.id}/webhooks/${webhook.id}`)
.expect(200)
.then((res) => {
expect(res.body).toMatchObject({
status: "success",
data: {
id: expect.any(String),
subscriberUrl: "https://example.com",
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
active: false,
payloadTemplate: "string",
teamId: org.id,
},
} satisfies TeamWebhookOutputResponseDto);
});
});
it("/organizations/:orgId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/webhooks/12993`).expect(403);
});
it("/organizations/:orgId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not belong to org", () => {
return request(app.getHttpServer())
.delete(`/v2/organizations/${org.id}/webhooks/${otherWebhook.id}`)
.expect(403);
});
});
@@ -0,0 +1,17 @@
import { IsNotEmpty, IsOptional, IsString } from "class-validator";
export class AssignOrganizationAttributeOptionToUserInput {
@IsOptional()
@IsString()
@IsNotEmpty()
readonly value?: string;
@IsOptional()
@IsString()
@IsNotEmpty()
readonly attributeOptionId?: string;
@IsNotEmpty()
@IsString()
readonly attributeId!: string;
}
@@ -0,0 +1,35 @@
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
import { AttributeType } from "@prisma/client";
import { Type } from "class-transformer";
import {
IsArray,
IsBoolean,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
ValidateNested,
} from "class-validator";
export class CreateOrganizationAttributeInput {
@IsString()
@IsNotEmpty()
readonly name!: string;
@IsString()
@IsNotEmpty()
readonly slug!: string;
@IsEnum(AttributeType)
@IsNotEmpty()
readonly type!: AttributeType;
@IsArray()
@ValidateNested()
@Type(() => CreateOrganizationAttributeOptionInput)
readonly options!: CreateOrganizationAttributeOptionInput[];
@IsBoolean()
@IsOptional()
readonly enabled?: boolean;
}
@@ -0,0 +1,11 @@
import { IsNotEmpty, IsString } from "class-validator";
export class CreateOrganizationAttributeOptionInput {
@IsString()
@IsNotEmpty()
readonly value!: string;
@IsString()
@IsNotEmpty()
readonly slug!: string;
}
@@ -0,0 +1,11 @@
import { IsNotEmpty, IsOptional, IsString } from "class-validator";
export class UpdateOrganizationAttributeOptionInput {
@IsString()
@IsOptional()
readonly value?: string;
@IsString()
@IsOptional()
readonly slug?: string;
}
@@ -0,0 +1,20 @@
import { AttributeType } from "@prisma/client";
import { IsBoolean, IsEnum, IsOptional, IsString } from "class-validator";
export class UpdateOrganizationAttributeInput {
@IsString()
@IsOptional()
readonly name?: string;
@IsString()
@IsOptional()
readonly slug?: string;
@IsEnum(AttributeType)
@IsOptional()
readonly type?: AttributeType;
@IsBoolean()
@IsOptional()
readonly enabled?: boolean;
}
@@ -3,19 +3,27 @@ import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/
import { EmailModule } from "@/modules/email/email.module";
import { EmailService } from "@/modules/email/email.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsOptionsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes-options.controller";
import { OrganizationsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes.controller";
import { OrganizationsEventTypesController } from "@/modules/organizations/controllers/event-types/organizations-event-types.controller";
import { OrganizationsMembershipsController } from "@/modules/organizations/controllers/memberships/organizations-membership.controller";
import { OrganizationsSchedulesController } from "@/modules/organizations/controllers/schedules/organizations-schedules.controller";
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller";
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
import { OrganizationsWebhooksController } from "@/modules/organizations/controllers/webhooks/organizations-webhooks.controller";
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/repositories/attributes/organization-attribute-option.repository";
import { OrganizationAttributesRepository } from "@/modules/organizations/repositories/attributes/organization-attribute.repository";
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
import { OrganizationsMembershipRepository } from "@/modules/organizations/repositories/organizations-membership.repository";
import { OrganizationSchedulesRepository } from "@/modules/organizations/repositories/organizations-schedules.repository";
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/repositories/organizations-teams-memberships.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import { OrganizationsUsersRepository } from "@/modules/organizations/repositories/organizations-users.repository";
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository";
import { OrganizationAttributeOptionService } from "@/modules/organizations/services/attributes/organization-attributes-option.service";
import { OrganizationAttributesService } from "@/modules/organizations/services/attributes/organization-attributes.service";
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service";
@@ -24,11 +32,14 @@ import { OrganizationsSchedulesService } from "@/modules/organizations/services/
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/services/organizations-teams-memberships.service";
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
import { OrganizationsUsersService } from "@/modules/organizations/services/organizations-users-service";
import { OrganizationsWebhooksService } from "@/modules/organizations/services/organizations-webhooks.service";
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { UsersModule } from "@/modules/users/users.module";
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
import { Module } from "@nestjs/common";
@Module({
@@ -60,6 +71,14 @@ import { Module } from "@nestjs/common";
OrganizationsEventTypesRepository,
OrganizationsTeamsMembershipsRepository,
OrganizationsTeamsMembershipsService,
OrganizationAttributesService,
OrganizationAttributeOptionService,
OrganizationAttributeOptionRepository,
OrganizationAttributesRepository,
OrganizationsWebhooksRepository,
OrganizationsWebhooksService,
WebhooksRepository,
WebhooksService,
],
exports: [
OrganizationsService,
@@ -71,6 +90,14 @@ import { Module } from "@nestjs/common";
OrganizationsMembershipService,
OrganizationsTeamsMembershipsRepository,
OrganizationsTeamsMembershipsService,
OrganizationAttributesService,
OrganizationAttributeOptionService,
OrganizationAttributeOptionRepository,
OrganizationAttributesRepository,
OrganizationsWebhooksRepository,
OrganizationsWebhooksService,
WebhooksRepository,
WebhooksService,
],
controllers: [
OrganizationsTeamsController,
@@ -79,6 +106,9 @@ import { Module } from "@nestjs/common";
OrganizationsMembershipsController,
OrganizationsEventTypesController,
OrganizationsTeamsMembershipsController,
OrganizationsAttributesController,
OrganizationsOptionsAttributesController,
OrganizationsWebhooksController,
],
})
export class OrganizationsModule {}
@@ -0,0 +1,71 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString, IsInt, IsEnum, IsBoolean } from "class-validator";
export const AttributeType = {
TEXT: "TEXT",
NUMBER: "NUMBER",
SINGLE_SELECT: "SINGLE_SELECT",
MULTI_SELECT: "MULTI_SELECT",
} as const;
export type AttributeType = (typeof AttributeType)[keyof typeof AttributeType];
export class Attribute {
@IsString()
@ApiProperty({ type: String, required: true, description: "The ID of the attribute", example: "attr_123" })
id!: string;
@IsInt()
@ApiProperty({
type: Number,
required: true,
description: "The team ID associated with the attribute",
example: 1,
})
teamId!: number;
@ApiProperty({
type: String,
required: true,
description: "The type of the attribute",
enum: AttributeType,
})
@IsEnum(AttributeType)
type!: AttributeType;
@IsString()
@ApiProperty({
type: String,
required: true,
description: "The name of the attribute",
example: "Attribute Name",
})
name!: string;
@IsString()
@ApiProperty({
type: String,
required: true,
description: "The slug of the attribute",
example: "attribute-name",
})
slug!: string;
@IsBoolean()
@ApiProperty({
type: Boolean,
required: true,
description: "Whether the attribute is enabled and displayed on their profile",
example: true,
})
enabled!: boolean;
@IsBoolean()
@ApiProperty({
type: Boolean,
required: false,
description: "Whether users can edit the relation",
example: true,
})
usersCanEditRelation!: boolean;
}
@@ -0,0 +1,11 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum } from "class-validator";
import { ERROR_STATUS } from "@calcom/platform-constants";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
export class BaseOutputDTO {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
}
@@ -0,0 +1,11 @@
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
import { Expose, Type } from "class-transformer";
import { ValidateNested } from "class-validator";
export class CreateOrganizationAttributesOutput extends BaseOutputDTO {
@Expose()
@ValidateNested()
@Type(() => Attribute)
data!: Attribute;
}
@@ -0,0 +1,11 @@
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
import { Expose, Type } from "class-transformer";
import { ValidateNested } from "class-validator";
export class DeleteOrganizationAttributesOutput extends BaseOutputDTO {
@Expose()
@ValidateNested()
@Type(() => Attribute)
data!: Attribute;
}
@@ -0,0 +1,22 @@
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsBoolean, IsInt, IsString, IsEnum, ValidateNested, IsOptional } from "class-validator";
import { AttributeType } from "@calcom/prisma/client";
export class GetSingleAttributeOutput extends BaseOutputDTO {
@Expose()
@ValidateNested()
@IsOptional()
@Type(() => Attribute)
data!: Attribute | null;
}
export class GetOrganizationAttributesOutput extends BaseOutputDTO {
@Expose()
@ValidateNested()
@Type(() => Attribute)
data!: Attribute[];
}
@@ -0,0 +1,33 @@
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsString, ValidateNested } from "class-validator";
class AssignOptionUserOutputData {
@IsString()
@ApiProperty({ type: String, required: true, description: "The ID of the option assigned to the user" })
id!: string;
@IsString()
@ApiProperty({ type: Number, required: true, description: "The ID form the org membership for the user" })
memberId!: number;
@IsString()
@ApiProperty({ type: String, required: true, description: "The value of the option" })
attributeOptionId!: string;
}
export class AssignOptionUserOutput extends BaseOutputDTO {
@Expose()
@ValidateNested()
@Type(() => AssignOptionUserOutputData)
data!: AssignOptionUserOutputData;
}
export class UnassignOptionUserOutput extends BaseOutputDTO {
@Expose()
@ValidateNested()
@Type(() => AssignOptionUserOutputData)
data!: AssignOptionUserOutputData;
}
@@ -0,0 +1,11 @@
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
import { Expose, Type } from "class-transformer";
import { ValidateNested } from "class-validator";
export class CreateAttributeOptionOutput extends BaseOutputDTO {
@Expose()
@ValidateNested()
@Type(() => OptionOutput)
data!: OptionOutput;
}
@@ -0,0 +1,11 @@
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
import { Expose, Type } from "class-transformer";
import { ValidateNested } from "class-validator";
export class DeleteAttributeOptionOutput extends BaseOutputDTO {
@Expose()
@ValidateNested()
@Type(() => OptionOutput)
data!: OptionOutput;
}
@@ -0,0 +1,29 @@
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsString, ValidateNested } from "class-validator";
class GetOptionUserOutputData {
@IsString()
@ApiProperty({ type: String, required: true, description: "The ID of the option assigned to the user" })
id!: string;
@IsString()
@ApiProperty({ type: String, required: true, description: "The ID of the attribute" })
attributeId!: string;
@IsString()
@ApiProperty({ type: String, required: true, description: "The value of the option" })
value!: string;
@IsString()
@ApiProperty({ type: String, required: true, description: "The slug of the option" })
slug!: string;
}
export class GetOptionUserOutput extends BaseOutputDTO {
@Expose()
@ValidateNested()
@Type(() => GetOptionUserOutputData)
data!: GetOptionUserOutputData[];
}
@@ -0,0 +1,11 @@
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
import { Expose, Type } from "class-transformer";
import { ValidateNested } from "class-validator";
export class GetAllAttributeOptionOutput extends BaseOutputDTO {
@Expose()
@ValidateNested()
@Type(() => OptionOutput)
data!: OptionOutput[];
}
@@ -0,0 +1,36 @@
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
import { ApiProperty } from "@nestjs/swagger";
import { IsString, IsInt, IsEnum, IsBoolean } from "class-validator";
export class OptionOutput {
@IsString()
@ApiProperty({
type: String,
required: true,
description: "The ID of the option",
example: "attr_option_id",
})
id!: string;
@IsString()
@ApiProperty({ type: String, required: true, description: "The ID of the attribute", example: "attr_id" })
attributeId!: string;
@IsString()
@ApiProperty({
type: String,
required: true,
description: "The value of the option",
example: "option_value",
})
value!: string;
@IsString()
@ApiProperty({
type: String,
required: true,
description: "The slug of the option",
example: "option-slug",
})
slug!: string;
}
@@ -0,0 +1,11 @@
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
import { Expose, Type } from "class-transformer";
import { ValidateNested } from "class-validator";
export class UpdateAttributeOptionOutput extends BaseOutputDTO {
@Expose()
@ValidateNested()
@Type(() => OptionOutput)
data!: OptionOutput;
}

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