Compare commits

..
125 Commits
Author SHA1 Message Date
Alex van Andel 6360e73901 chore: Release v4.6.0 (#17121)
release.yml / release (push) Canceled after 0s
2024-10-15 17:53:06 +00:00
7e44e686e8 feat: attributes filter / refactor of user data table (#17014)
* WIP restored from .git cache

* fix exports

* sortable row model

* feat column visibility component

* wip filters with nuqs

* pull in unique values from table into filters

* correctly assign filters via v/f

* inital selection bar refactor

* data-table selection bar + optmistic update of delete

* dynamic link

* migrate member list table to new data-table

* total list shows filtered value > db valuie

* add filters for attributes

* type errors

* make content bigger on lg

* add mb-6 to teams user datatable to match spacing spec

* correctly render multi-badge

* fix: masss asignment optimistic UI

* fix type errors

* remove log

* fix toolbar type error

* chore: Remove debug artifact

* type errors

* Update apps/web/public/static/locales/en/common.json

* use max-w-fit

* chore: Remove unused translation now we don't specify 'mass' in assign

* perf: fix: use the onBlur event to prevent focus loss whilst the list is rerendering

* Move the data-table exports together in the main barrel, then import

* fix exports that were lost in a merge

* fix exports that were lost in a merge

* fix groupteammapping/availbilityslider

* fix overflow problems

* add scrollbar-thin class

* fix type error

* user serverside values for faceted filters

* pass filters to serverside

* filter serverside

* fix team server side filter

* add loaded x of y

* attributes icon change

* correct implementation for text/input attr optimistic

* type check fixes

* fix platform checks

* fix types again

* fix types again

* fix types again

* add use client

* add use client

* fix-types

* fix: Add missing translation in EN

* fix e2e tests via testid

* fix e2e tests via testid

* fix: Member invite popup not popping up

* Update copyInviteLink to new-member-button testid

* Hopefully fix test ids this time

* fix: Use the right buttons on the right pages

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-10-15 17:45:11 +00:00
Udit Takkar 0b914bef79 fix: update meeting token if expired (#17096) 2024-10-15 10:41:18 -07:00
a48d190af3 feat: #15416 Expand round robin availability beyond default schedule (#15618)
* add feature to change host schedule

* update for type-check:ci

* Update SingleForm.tsx for linterr

* update as per design review

* update for rerendering

* migration files

* On Delete set null for schedule

if schedule is deleted, no need to delete the host

* updated schema

* added testcases

* checking permissions before

* updated for comments

* update for initial value

* updated and cleanup

* set initial value

* update after merge

* updated nit style

* update for console warning and added comments

* nit/chore

* chore

* update for typechecks

* update for scheduleId param after merge

* update after merge or new changes in metadata.config

* update after weights functionality

* updated description text and column name as per review suggestion

* resolved merge conflicts

* fix typecheck due to changes in getScheduleSchema

* update after merge having refactor

* update refactored EventTypeWebWrapper after merge

* updated props

* updated trpc err code and added log

* update due to refactor in EventAvailabilityTab

* perf: Optimise TS type + select only the data that's needed for a specific function

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-10-15 17:36:00 +01:00
Rajiv SahalandKeith Williams c21f0caecf chore: revamp platform docs (#17117)
* all atoms have standalone page

* restructure tabs for platfom pages

* remove unused page

* add missing icons

* cleanup intro page for platform

* update loom links

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-10-15 15:21:09 +00:00
Anik Dhabal Babu 1f42a152b0 fix: link is not rendering properly for SMS and WhatsApp workflows (#17116) 2024-10-15 15:11:29 +00:00
Hariom Balhara b8f0cc6fa1 fix: Add back missing Any in and Not in operators for Single Select (#17102)
* fix: Missing any_in and not_anhy_in opperators

* Add tests
2024-10-15 15:07:40 +00:00
Anik Dhabal Babu c6f84e6c4b fix: Issue chargin no show fee (#17066) 2024-10-15 15:05:53 +00:00
Morgan 9c3f4ec055 feat: allow delete event-type platform atoms (#17065)
* feat: allow delete event-type platform atoms

* fixup! feat: allow delete event-type platform atoms
2024-10-15 14:40:57 +00:00
Alex van Andel f0cfbede7e fix: Fix TS error + set isPending on Button (#17118) 2024-10-15 13:59:31 +00:00
Keith Williams e2a825a9a0 Revert "feat: update github workflows to install playwright in a separate job (#17070)" (#17114)
This reverts commit 0b3b268eed.
2024-10-15 08:23:52 -03:00
Keith Williams 963a2a0b76 fix: Playwright install (#17113) 2024-10-15 07:58:38 -03:00
Benny Joo 4037367f8c fix: theme flickering (#16903)
* fix: theme flickering

* finalise
2024-10-15 11:13:39 +01:00
Aditya GhadgeandKeith Williams 0b3b268eed feat: update github workflows to install playwright in a separate job (#17070)
* feat: update github workflows to install playwright in a separate job

* Update .github/workflows/pr.yml

* Update .github/workflows/pr.yml

* Update .github/workflows/pr.yml

* Update .github/workflows/pr.yml

* Update .github/workflows/pr.yml

* Update .github/workflows/pr.yml

* Update .github/workflows/pr.yml

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-10-15 10:09:38 +00:00
amrit 2acb5559dc refactor: improve variable logic (#17086)
* Improve app processing with async/await and dynamic imports, also fix eslint errors.

* refactor

* added back a comment
2024-10-15 06:35:26 -03:00
Keith Williams be1c6eb65c fix: PR update workflow duplicate job (#17108) 2024-10-15 05:30:22 -03:00
Keith Williams feefa26c90 chore: Add docs build (#17101)
* chore: Add Docs build to CI

* Added build-docs to flow

* Updated to run in the /docs folder

* Made web app build have consistent name

* Added a kill after the command runs

* Test the build

* Added it to the PR flow
2024-10-15 05:08:32 -03:00
Avishay Maor 2c25f88100 Update constants.ts - fix crashing by adding optional chaining for this value (#17103) 2024-10-15 07:32:27 +00:00
2be9fc8bc0 fix: email-address with apostrophe not accepted (#15535)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-10-15 00:21:26 +00:00
Peer Richelsen 5927b42560 Update mint.json (#17085) 2024-10-14 18:48:03 -03:00
Rajiv SahalandMorgan 5726d18027 feat: member management for platform (#16803)
* init members page for platform

* add missing icon

* add members tab to sidebar

* use data from useMe query instead of useSession

* return user profiles since platform users are stored there

* small refactor

* fixup

* remove unused logs from console

* fixup

* resolve merge conflicts

* add missing tabs for members and billing

* update schema

* fixup

* memoise selection options for org and platform

* update onSubmit handler

* resolve merge conflicts

* add members tab

* improve team billing view when user not subscribed

* update members tab

* add function to find if user is admin of team

* update invite members handler to handle platform user as well

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-10-14 21:19:02 +03:00
Anik Dhabal Babu 3312d9cfc6 fix: 500 status code visible when cancelling event (#16957)
* fix: 500 status code visible when cancelling event

* update
2024-10-14 12:58:28 -04:00
Steve Xu c140b01548 fix: Memory leak with cal embed modal box (#17016) 2024-10-14 12:42:57 -04:00
Lauris Skraucis c2085c6be9 refactor: v2 recurring bookings allow specifying recurrence count (#16989)
* fix: CreateBookingInputPipe

* refactor: allow specifying recurrence count

* Update input.service.ts
2024-10-14 17:59:53 +03:00
sean-brydon b8b9d04897 up stripe trails (#17092) 2024-10-14 13:01:01 +00:00
Udit TakkarandSyed Ali Shahbaz 4497c4ce0b perf: improve get bookings performance (#16303)
* refactor: improve get bookings performance

* feat: use map to optimise

* chore: save

* chore: add authorization

* chore: remove condition

* chore: improvements

* chore

* chore: remove return

* chore: use Promise.all

---------

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2024-10-14 17:57:58 +05:30
Anik Dhabal Babu cd06141816 fix: improved code for meeting link is not updated and broken ui in edit location dialog issue (#16013)
* improvise code of issue#15497

* Update editLocation.handler.ts

* update

* Update EditLocationDialog.tsx

* Update editLocation.handler.ts
2024-10-14 08:56:17 +00:00
Rajiv Sahal 013646bf2c fix: atoms tooltip (#17073)
* add tooltip classname prop for styling

* fix incorrect styling

* update props for connect buttons
2024-10-14 09:37:19 +03:00
Anik Dhabal BabuandAlex van Andel 12bbbe3617 fix: Daterangepicker is not opening in some browsers (#17087)
* fix: daterangepicker issue

* update

* fix: useTeamPlan isn't relevant on self-hosted

* chore: Revert UpgradeTeamsBadge fixes, unrelevant to this PR

* fix: Fix button width for other languages

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-10-14 06:46:37 +05:30
Peer Richelsen 2c7c9fd716 feat: telli.so app (#17083) 2024-10-12 17:34:23 +00:00
Keith Williams 11fc2a738a fix: Docs build issue (#17082) 2024-10-12 16:53:43 +00:00
Calcom BotandReplexica 44ab25b4a0 feat: update translations (#17076)
Co-authored-by: Replexica <support@replexica.com>
2024-10-12 13:30:54 -03:00
Ezhil Shanmugham 4a2c0cb164 fix: lock on user view (#17077)
* fix: lock on user view

* fix: lock on user view
2024-10-12 13:54:12 +00:00
Joe Au-YeungandUdit Takkar b1d9e2b84c feat: Salesforce choose how to save attendees & other options (#17009)
* Refactor get SF user and create event

* Add new Salesforce options

* Pass app options to CrmService

* Add record enum

* Add creating new lead and contact under an account

* Handle if the contact already exists but not connected to account

* Create a lead if an account doesn't exist

* Merge fix

* Refactor - add generateCreateRecordBody

* Type fix

* Clean up

* Add getAppOptions to other CRM services

* Type fixes

* Fix typo

* feat: Salesforce round robin skip - choose which entity to search on (#17038)

* Add option to search which entity for the owner

* Add logic to search entity for skipping RR assignment

* Type fixes

* Disable license

* Skip license check in component

* Revert license changes

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-10-12 10:02:27 +00:00
Carina WollendorferandCarinaWolli 47b9e8a8ac fix: attendee name workflow variables in spanish (#17068)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-10-11 09:12:23 -07:00
Benny JooandAlex van Andel 42265021fe chore: settings/my-account - remove pages router (#17056)
* remove pages router and use app router

* fix page

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-10-11 16:30:03 +01:00
Anik Dhabal BabuandUdit Takkar 0b0fee3985 fix: phone number input field is missing from mail (#17035)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-10-11 11:17:18 +00:00
Benny JooandAlex van Andel 5718cc5e17 chore: remove react-query, update next.js to v13.5.6 (#17057)
* chore: remove react-query as unneeded

* chore: Re-gen lockfile

* chore: Bump next.js to v13.5.6

* yarn.lock regen after apps/console next version bump

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-10-11 10:06:20 +00:00
Benny Joo 361889ded7 add node-gyp as dev dependency (#17058) 2024-10-11 10:28:37 +01:00
Rajiv Sahal 2260ddcebc add missing tab for billing in navigation (#17060) 2024-10-11 12:42:32 +05:30
Keith Williams 80c3b91077 chore: Update CI to not run all checks for docs/translations (#17054) 2024-10-11 09:44:13 +05:30
Calcom BotandReplexica a11cf130bf feat: update translations (#17051)
Co-authored-by: Replexica <support@replexica.com>
2024-10-10 20:46:33 +00:00
Omar López f9d6ea2a00 chore: v4.5.11 (#17053)
release.yml / release (push) Canceled after 0s
2024-10-10 13:23:28 -07:00
Udit Takkar 50f6aa5f58 perf: refactor and replacing viewer.teams.getMinimal with viewer.teams.get (#16552) 2024-10-10 12:51:07 -07:00
Keith Williams 132746af74 perf: Use UNION to get public user event types (#17052) 2024-10-10 19:39:32 +00:00
395381ddcc feat: automatic no show (#16727)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2024-10-10 10:57:04 -07:00
Keith Williams ef88effd9f perf: Add index to EventType for parentId (#17049) 2024-10-10 17:46:12 +00:00
Calcom BotandReplexica 673a2be7c7 feat: update translations (#17042)
Co-authored-by: Replexica <support@replexica.com>
2024-10-10 17:19:27 +00:00
Shrey TannaandAlex van Andel b1cd3e940e feat: Add Eastern and Eastern Pacific time zones #16880 (#16974)
* add eastern and eastern pacific timezones

* fixed issue while running tests.

* fix: Find Braz..il instead of Brasil

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-10-10 16:26:17 +00:00
Alex van Andel 711bec8d71 chore: Add typings for DOMPurify (#17047) 2024-10-10 17:17:13 +01:00
Syed Ali Shahbaz 7529e331a3 Pick attendee id in response (#17041) 2024-10-10 15:40:58 +00:00
Peer RichelsenandAlex van Andel cc147d0797 chore: docs changes (#17031)
* Update introduction.mdx

* added new logo

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-10-10 15:24:21 +00:00
c8e8800647 chore: Update EWS package (#16950)
* Update ews package

* Hide exchange version if using NTLM

* Remove instance of gzip

* Update yarn.lock file

* Updated yarn.lock

* Update yarn.lock

* Update yarn.lock

* Update yarn.lock (again)

* chore: Increase readability of [number] indexes by using find

* chore: Async not needed

* fix: Move 'use client' to appropriate file

* refactor

* install deasync directly as dev dependency

* fix type error

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2024-10-10 15:48:58 +01:00
99caae8665 feat: advanced tab platform wrapper (#16941)
* fixed error in advanced tab atom build

-- used `DOMPurify` instead of `sanitize-html`

* hide BookerLayoutSelector for platform and removePlatformClientIdFromEmail

* fixup! Merge branch 'main' into event-advanced-platform-wrapper

* undo linting changes

* undo linting changes

* fix: active state of tabs items

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-10-10 17:09:03 +03:00
Hariom Balhara 1f08bc5a4f fix: DatePicker not showing up in mobile mode when hideEventTypeDetails is true (#17037) 2024-10-10 18:09:53 +05:30
Morgan 5594e5c713 feat: google meet conferencing api v2 (#17023)
* feat: google meet conferencing api v2

* feat: add set default google meet conferencing app

* fixup! feat: add set default google meet conferencing app
2024-10-10 09:28:53 +00:00
Rajiv SahalandMorgan 34afc7b674 feat: additional calendar selector for calendar settings atom (#16874)
* update translations

* fixup

* add onSuccess prop

* add additional calendar selector prop to add multiple calendars

* update props for calendar settings atom

* pass caledar redirect urls for google and apple calendar redirects

* fix merge conflicts

* add onSuccess prop to connect atoms

* fixup

* fixup

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-10-10 09:02:40 +00:00
Somay Chauhan 513a58c91c feat: event type tabs platform wrapper (#16891)
* added event-type platform wrappers

* added weekly

* Update RecurringEventController.tsx
2024-10-10 10:55:13 +03:00
Syed Ali Shahbaz 375b9190a3 add slot endpoint (#17034) 2024-10-09 20:59:10 +00:00
Anik Dhabal Babu 83d7bf322a fix: add missing options for team memberlist table (#17017)
* fix: missing option for team memberlist table

* update
2024-10-09 18:23:45 +01:00
06429e27fb test: Add Attributes Routing e2e and some more unit tests (#17032)
* Initial commit

* routingForm to Booking a particular team member working

* Happy path working

* Fixes

* Fix router query params forwarding

* Add basicConfig within app

* Tests

* More tests

* Update packages/app-store/routing-forms/components/SingleForm.tsx

Co-authored-by: Omar López <zomars@me.com>

* WIP

* Fix lint issues

* Fix tests -TS and add new logic

* Add getEventTypeRedirectUrl tests

* Remove booking title changes code

* More changes

* Add tests for getRoutedUsers

* More tests

* Update testUtils.ts

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-10-09 22:47:16 +05:30
Benny Joo 90bcd711a9 fix: padding in TeamPill (and hence TeamRole) (#17030) 2024-10-09 15:21:26 +00:00
Keith Williams 8d61b452b1 chore: Removed development only docs tags (#17028) 2024-10-09 11:58:58 -03:00
Hariom BalharaandOmar López 03d0b98155 feat: Skip contact owner even if it matches attribute routing (#17015)
Co-authored-by: Omar López <zomars@me.com>
2024-10-09 07:30:12 -07:00
de503add83 feat: add nextcloud talk app (#16455)
* Initial draft of nextcloud app

* Add video adapter for creating a new conversation via API

* Make links dynamic, add nc user and pwd for auth, fix link creation

* Add nextcloudTalk app to yarn.lock

* Fix screenshot name

* Add OAuth2 handler

* Fix type for booking reference and fix access token header param, (for creating and deleting a meeting)

* Remove comment

* Add README.md file

* Fix redirect URL for authenticating Nextcloud user

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-10-09 17:52:30 +05:30
Keith Williams 1a4cf5f91d fix: URLs to images in docs (#17022) 2024-10-09 11:48:26 +00:00
Calcom BotandReplexica ed53645783 feat: update translations (#17021)
Co-authored-by: Replexica <support@replexica.com>
2024-10-09 10:37:44 +00:00
sean-brydon c7b1fbdf67 feat: mass assign attributes (#16995)
* UI popover on user table

* fix multi select state

* mass assignment

* add i18n
2024-10-09 12:02:48 +02:00
sateshcharan 6d7f7108e5 fix schema.prisma path (#17018) 2024-10-09 08:55:30 +00:00
Hariom Balhara d9aab9088d feat: Support headless router as well in embed (#17012)
* Support /router/embed path and add Headless router usecase in playground

* Fix broken seeded-form testing wrong thing
2024-10-09 13:24:45 +05:30
Hariom Balhara c80daf43b8 Fix prettier error and EMailField import error in website (#17011) 2024-10-09 08:30:29 +02:00
Omar López 2bdf65ce8c fix: billing webhook secret (#17005)
* fix: billing webhook secret

* Update __handler.test.ts
2024-10-08 21:26:39 +00:00
Keith Williams 343e2ac9d7 chore: v4.5.10 (#17001)
release.yml / release (push) Canceled after 0s
2024-10-08 16:59:25 -03:00
Benny Joo 670b82fd23 fix: add fallback to data needed in SettingsLayout (#17004) 2024-10-08 15:51:38 -04:00
Keith Williams e6f81d09c8 chore: Update PR template to account for new docs (#17003) 2024-10-08 15:35:23 -03:00
Benny Joo 8f91ffe692 Revert "remove pages router and use app router (#16793)" (#17002)
This reverts commit 664a92a1d0.
2024-10-08 15:00:43 -03:00
Benny Joo f5dbdae4a9 fix: Close OOO Create Modal on backdrop click (#17000) 2024-10-08 17:42:28 +00:00
Calcom BotandReplexica e99f6048d6 feat: update translations (#16999)
Co-authored-by: Replexica <support@replexica.com>
2024-10-08 23:08:47 +05:30
654a204878 feat: Attribute based routing from Team Routing Form to Team Event(with assigned users matching the attribute logic) (#16823)
* Initial commit

* routingForm to Booking a particular team member working

* Happy path working

* Fixes

* Fix router query params forwarding

* Add basicConfig within app

* Tests

* More tests

* Update packages/app-store/routing-forms/components/SingleForm.tsx

Co-authored-by: Omar López <zomars@me.com>

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2024-10-08 21:12:32 +05:30
Syed Ali Shahbaz 7928933852 fixes (#16990) 2024-10-08 15:36:16 +00:00
Lauris SkraucisandMorgan Vernay c4cdba1015 refactor: throttler rate limits for oauth client & access token (#16962)
Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-10-08 17:34:41 +03:00
Benny Joo 5ed36a7fe1 chore: remove react code in getLayout (#16997)
* chore: no need to pass title, description in getLayout

* acutally, we do
2024-10-08 14:22:26 +00:00
Joe Au-YeungandBenny Joo e77a2f6963 fix: Salesforce assign event to organizer (#16971)
* Refactor get SF user and create event

* address code nit

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2024-10-08 14:06:51 +00:00
Lauris Skraucis a10d3599e2 refactor: platform bookings hooks & bookings API (#16963)
* chore: delete unused bookings hook

* refactor: group bookings hooks in folder

* refactor: rename useGetBooking, useGetBookings to useBooking, useBookings

* refactor: booking inputs and outputs

* chore: make bookings controller work with new inputs and outputs

* make examples app work

* refactor: deprecate fields instead of removing them

* readd eventtypeid

* swagger regenerate

* make recurring booking backwards compatible

* fix test

* update changelog

* swagger update

* fix import

* chore: update lock file
2024-10-08 14:05:28 +02:00
Alex van Andel d99f171597 perf: Only render the TimezoneChangeDialog when needed (#16986)
* perf: Only render the TimezoneChangeDialog when needed

* fix: Import error

* Minor style fixes
2024-10-08 10:41:53 +01:00
Syed Ali ShahbazandKeith Williams da55306a5a chore: add dev docs on mintlify (#16935)
* --init

* progress...

* add more docs

* bunch of changes and added back platform

* development guides added

* more guides in develop with cal

* self hosted WIP

* deployments

* fix deployment steps

* self-hosted guides

* ..

* Removed the development only sections

* Removed 'with Cal.com'

* use steps component for steps

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-10-08 02:32:16 -03:00
Benny Joo 66378160df chore: remove log (#16981) 2024-10-07 22:10:25 +00:00
Anik Dhabal Babu 8aee8d9d3b fix: SMS reminders are being sent out with an HTML code (#16966) 2024-10-07 17:51:04 -04:00
Calcom BotandReplexica 79e33a4d26 feat: update translations (#16972)
Co-authored-by: Replexica <support@replexica.com>
2024-10-07 15:37:32 -03:00
Carina WollendorferandCarinaWolli b5fa43f086 feat: checkbox to count managed events for team-wide limits (#16923)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-10-07 11:36:50 -04:00
Joe Au-Yeung 0d757039f7 chore: Fixing ICS file in workflows & ICS calendar descriptions [CAL-4409] (#16731)
* Remove title and subtitle from ICS calendar description

* Remove unused method

* Use generateIcsString function for workflows

* Type fixes

* Refactor typing of CalEventParser

* WIP

* Type fixes

* Fix test

* Fix tests

* type fixes

* Remove duplicate code before `scheduleEmailReminder`
2024-10-07 10:50:50 -04:00
Lauris Skraucis 44f6f2e320 refactor: v2 throttler guard (#16922)
* chore: add logs

* refactor: replace deprecated redis.setex with redis.set

* chore: log response headers
2024-10-07 11:02:41 +00:00
Benny Joo d8eb1df61d fix: add suppressHydrationWarning to app router layout (#16960) 2024-10-07 12:40:09 +02:00
Lauris Skraucis ef7cac6c50 fix: booker atom phone booking field (#16945)
* fix: booker atom phone booking field

* chore: show usual booker in examples app

* refactor: import styles file manually
2024-10-07 12:39:54 +02:00
Benny Joo 21dd44733f fix: make theme logic safe and add better e2e tests (#16959)
* fix: improve theme logic

* remove null from ThemeLabelProps value

* add better e2e tests
2024-10-07 11:23:30 +01:00
Lauris Skraucis b0bfec7247 feat: Booker atom read only fields (#16904)
* refactor: include user-readonly in BookingFields

* feat: add read only name and email booking fields

* feat: Booker atom prop for read only name and email

* refactor: simplify props type
2024-10-07 15:03:07 +05:30
Lauris Skraucis ba5b9e951a feat: Booker atom enable hiding event details (#16906)
* refactor: Booker component hideEventTypeDetails hides sidebar

* feat: Booker atom hideEventTypeDetails prop

* fix: redundant border appearing if event details hidden

* Revert "fix: redundant border appearing if event details hidden"

This reverts commit 63122a906a8e41b54074387e1a6a4f0e4489a8a9.

* fix: border
2024-10-07 09:52:47 +02:00
Lauris Skraucis 5e3865b645 docs: v2 mintlify update (#16961) 2024-10-07 06:54:36 +00:00
Rajiv Sahal a02240b579 fix: add missing ApiOperation tag v2 (#16947)
* add missing api operation tag

* update openapi spec for docs
2024-10-06 20:04:25 -03:00
Benny Joo 376caa412d fix: DOM attribute warning in event-types edit page (#16953) 2024-10-05 03:39:11 +00:00
Benny Joo 8722745185 chore: improve readability of some tests (#16954) 2024-10-05 08:49:29 +05:30
+12 a4a3eb5ea9 refactor: Billing service (#15366)
* WIP: Billing service

* WIP

* Update __handler.ts

* Update index.ts

* WIP

* WIP

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

* WIP

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

* Update delete.handler.ts

* Update delete.handler.ts

* Update internal-team-billing.ts

* WIP

* Type fixes

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

* WIP

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

* Discard changes to apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.ts

* Discard changes to packages/app-store/skype/config.json

* style: update text area (#15934)

* chore: update text-editor input

* chore: update text-editor input

* final update

* Update Editor.tsx

---------

Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>

* fix: #15697 Assignment warning modal poping up ,even when hosts are added . (#15699)

* removed reduntand check on assignedusers

* update for managed eventtype

* formatted

* comments

* Updated comments

* extracted checkForEmptyAssignment and added tests

* updated comments

* updated to show dialog also when not yet saved

* update translations

* updated comments

* updated translation

* updated types

* chore: update naming and comments

* Delete apps/web/lib/checkForEmptyAssignment.ts

deleted since file was renamed

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

deleted as the file is renamed to be more descriptive

* update to do teamcheck before

* chore: revert name to CheckForEmptyAssignment

* updated check conditions

* updated useEffect dependencies

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>

* fix: getting an error when a team member tries to leave (#15925)

a team

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

* feat: add transitions to more components (#15916)

* add transitions to more components

* chore: Remove duplicate class

* fix: Change minimum number of teams required for org self-serve (#15955)

* fix: Change minimum number of teams required for org self-serve

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

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

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

* Update packages/lib/constants.ts

* Update turbo.json

---------

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* feat: pwa ready and add shortcut for quick access (#15957)

* chore: Update site.webmanifest with additional display properties

* chore: Update site.webmanifest with additional display properties

* Update apps/web/public/site.webmanifest

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* i18n: update translations for "by proceeding, ..." (#15921)

* feat: Add title attribute to iframe in embed.ts (#15944)

* feat: Add title attribute to iframe in embed.ts

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

---------

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

* fix: Assignment reminder modal appears when deleting a team event type (#15544)

* fix: Assignment reminder modal appears when deleting a team event type

* update

* Update EventTypeSingleLayout.tsx

* Update event-types-single-view.tsx

* update

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

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>

* fix: Add CORP and COEP headers to embedded pages (#15275)

* Add CORP headers

* Improve test

* Add unit tests as well

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* feat: No-show bookings - do not count towards a member's round robin assignment (#15323)

* feat: No-show bookings - do not count towards a member's round robin assignment

* filter no show organizer

* remove logs

* fix: filter bookings where host attendee is no show

* fix: use not condition for noShow

* chore: test

* add default false for noShowHost

* Revert "chore: test"

This reverts commit c914a8fc4a14ca9c7d8c4a08ff8b0f463789171c.

* chore: integration test

* fix: noShowHost not working for null

* Update packages/lib/server/getLuckyUser.ts

* chore: update test description

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: gh0st91848 <gh0st91848@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* refactor: v1 orgs unlimited teams (#15908)

* refactor: v1 orgs unlimited teams

* fix: response message ownerId

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* fix: refresh connect atoms on token change (#15933)

* added dropdown

* change comment

* fix: added types

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* fix: Next.js bundle analysis (#15903)

* fix: Next.js bundle analysis

* Added the main checkout back

* fix: type in webhook payload (#15912)

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

* fix: auto accept (#15975)

* fix: auto accept

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

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

---------

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

* fix: Broken GitHub workflows (#15979)

* test: Delete credential endpoint (#15322)

* Init

* Add mock trpc context

* Intro mockDatabaseClient

* Introduce mockDatabaseClient class

* Add delete video app test

* Add calendar test

* Remove unused func from bookingScenario

* Remove console.log

* Add app repository

* Add createMany method to event type repository

* Remove instance of MockDatabaseClient from video test

* Add destination calendar repository

* Remove instances of MockDatabaseClient

* abstract logic to own handler

* Remove dev dependency

* Clean up

* Type fix

* Pull yarn.lock from main

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>

* fix: #15967 Show better error when wrong variable is used in custom event name (#15974)

* update to show exact err msg with custom event name

* updated tests for validateCustomEventName

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>

* WIP

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

* Update yarn.lock

* Update publish.handler.ts

* Update internal-team-billing.test.ts

* rename create to init

* remove sync services

* wip tests for delete team

* wip tets

* wip tests

* tests/internal-test

* remove return that is inaccessable

* remove team calls to use repository

* remove redudant file

* update team repo

* add stripe mocks and test

* fix constants

* tests to main stripe handler

* remove logs

* fix constants

* Update packages/features/ee/billing/api/webhook/_customer.subscription.deleted.team-plan.ts

Co-authored-by: Omar López <zomars@me.com>

* move workflow logic to a service

* use static method > creating new class

* fix: remove http code deep in repo + tests

* Feedback

* Update index.ts

* Type fix

* Lazy load fixes

---------

Signed-off-by: zomars <zomars@me.com>
Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>
Co-authored-by: Rohan Advani <122167726+RNAdvani@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: Vijay <vijayraghav22@gmail.com>
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Hichem Fantar <hichemfantar2049@gmail.com>
Co-authored-by: Souptik Datta <souptikdatta2001@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Johannes Maendle <141119154+jomaendle2@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: gh0st91848 <gh0st91848@gmail.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean <sean@brydon.io>
2024-10-04 13:44:50 -07:00
Benny Joo b560475d89 fix: flaky one in local e2e test (#16949) 2024-10-04 20:36:52 +00:00
Calcom BotandReplexica 4872daef60 feat: update translations (#16943)
Co-authored-by: Replexica <support@replexica.com>
2024-10-04 20:15:18 +00:00
Anik Dhabal Babu dfa92f5cb6 fix: currency conversion error for paypal (#16917)
* fix: currency conversion error for paypal

* type-error
2024-10-05 01:11:09 +05:30
Benny Joo babb1a3bf7 fix annoying warning due to key spread (#16948) 2024-10-04 19:39:20 +00:00
Benny Joo 7b5a2726ab fix: translations for insights, routing-forms, apps store, etc (#16933)
* remove outdated translations for insights

* use i18n strings for insights

* use i18n strings for routing forms

* use i18n strings for apps store

* fix more pages

* refactor

* fix
2024-10-04 09:45:51 -03:00
sean-brydon 7d4653296e add trial days to stripe teams checkout (#16940) 2024-10-04 12:42:51 +00:00
Hariom Balhara 644fb38f87 test: more Embed Page Params filtering tests (#16937)
* Add rescheduleUid as config param scenario

* Restructure test buttons

* Use correct namespace as per rename
2024-10-04 12:01:59 +01:00
Ash Davis 743b155a88 fix: rescheduledBy to carry into success url redirect (#16931) 2024-10-04 10:59:52 +01:00
Benny JooandAlex van Andel 664a92a1d0 remove pages router and use app router (#16793)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-10-04 10:28:21 +01:00
Hariom Balhara 8ae71f6160 fix: Certain query params being filtered out from embed (#16936) 2024-10-04 12:59:45 +05:30
Omar López ab1eab9c54 chore: v4.5.9 (#16926)
release.yml / release (push) Canceled after 0s
2024-10-03 12:26:35 -07:00
MuhammadandAnik Dhabal Babu a64b7752c9 fix: Tooltip must be used within TooltipProvider (#16918)
* fixes #16833

* Update index.tsx

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-10-03 15:37:11 +00:00
Keith WilliamsandSyed Ali Shahbaz be728e9c01 feat: add Mintlify docs (#16787)
* feat: Add Mintlify docs

* Fixed the mint.json for v2

* Fixed versioning issue

* Add titles to ICS feed endpoints

* Added errors to v1

* Added rate-limit to API v1

* feat: added v1-v2 differences and v1 auth

* Trying to eliminate reasons for v1 not showing on publication

* Used the swagger openapi validator

* remove deprecated v1 endpoints

* Moved api docs into new folders without api-reference

* Renamed api to api-reference

* removed the versioning

* Got rid of anchor warnings

* Fixed naming

* Just as a test - using the same v2 openapi spec for v1 reference

* Removed 'externalDocs' from v1

* Removed securitySchemes

* Updated the diff between v1/v2

* Added v1 in front of /attendees to test

* Moved paths up and removed externalDocs

* Testing - Added content to the response

* Reduced paths to 1

* Testing: anything at this point

* Fixed validation

* Moved the other .mdx files to see if that's disrupting something

* Trying with a tab

* Empty tags array

* Added back servers field

* Removed all other schemas

* Removed v1 for now

* Added platform docs

* Language fixes

* Added v1 back

* add API V1 response for attendees

* remove unnecessary endpoint docs in API V1

* Removing platform and API for now

* Updated v2 openapi doc

---------

Co-authored-by: Syed Ali Shahbaz <alishahbaz7@gmail.com>
2024-10-03 15:26:01 +04:00
Benny Joo 2146c04444 fix: i18n for app router (#16909)
* fix: translations for app router

* fix build error
2024-10-03 00:32:33 +01:00
Calcom BotandReplexica 0b94473c7f feat: update translations (#16911)
Co-authored-by: Replexica <support@replexica.com>
2024-10-02 17:44:36 -04:00
2a1dc619ee feat: Multiple Private links per Event Type (#15896)
* feat: Change hashedLink field to array

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

* feat: Implement core single-use links logic

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

* fix: Get private link from list of hashedLinks on event list page

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

* fix: Fix defalut single-use links form field value

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

* fix: Fix some type errors while compiling

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

* fix: Use onChange callback instead of setting form values directly

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

* feat: Add e2e tests for single-use links

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

* refactor: Refactor single-use-links admin interface

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

* fix: Fix settings toggle bugs

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

* chore: Remove redundant inline comments

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

* fix: Remove debug statements

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

* chore: Remove single-use links e2e tests

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

* feat: Remove private-link feature and override with single-use-links

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

* fix: Fix type errors

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

* refactor: Remove redundant migrations

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

* fix: Add managedEvents restriction

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

* fix: Fix single-use-links for org

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

* fix: Fix recommended UI changes

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

* fix: Fix duration not respected for private links

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

* fix: Fix team event page title

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

* feat: Add logic to copy each single-use-links from event listing page

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

* feat: Add single-use links support for managed event types

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

* fix: Fix some type errors

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

* fix: Fix type errors

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

* refactor: Disable adding new single-use links when field is locked

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

* fix: Address feedbacks

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

* refactor: Hide add new link button for locked single-use link field

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

* chore: Use empty string for single-use link fallback value condition

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

* fix: Fix org single-use links in listing page

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

* feat: Auto toggle unlock button for managed event's single-use links option

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

* Revert "feat: Auto toggle unlock button for managed event's single-use links option"

This reverts commit 5a2ed6f0bf428d52de153335499f02f42ccdf18f.

* chore: Disable parent toggle for single-use-links

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

* feat: Remove tooltip and give info message below field

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

* add tooltip

* tooltip message

* fix: Fix typo

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

* fix: Fix typescript type errors

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

* fix: Fix few unit tests

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

* refactor: Remove redundant query

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

* merge conflict fix

* More merge conflict fix and refactor

* fix tests, code refactor

* fix: type check

* fix: duration config and keep links when updating event type

* fix: e2e

* refactor: Replace single-use-links with multiple-private-links

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

---------

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Amit91848 <samit91848@gmail.com>
2024-10-03 00:58:54 +05:30
Benny JooandKeith Williams 48a2fc2f67 fix: translations for sidebar (#16910)
* fix: translations for sidebar

* Update apps/web/public/static/locales/en/common.json

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-10-02 18:21:25 +00:00
4553455098 chore: refactor shell to individual files (#16893)
* banners

* useAuthHooks

* fixes to redirect and banner

* extract useIntercom to custom hook

* use app theme

* extract user-dropdown to new component

* Navigation Item

* navigation and profile dropdown

* Fix import

* navigation and sidebar

* fix type errors

* fix banners being an async call

* fix types

* fix banner prop type

* fix mobile nav item import

* fix banner types

* (revert) layout banner render method to fix type error

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-10-02 15:35:56 +01:00
b4f1b5aebe refactor: handleNewBooking #4 (#15673)
* refactor: handleNewBooking #3

* refactor: create booking factor

* refactor: handleNewBooking

* refactor: seats and rescheduleUId

* chore: remove comment

* fix: type err

* chore: add missing statement

* chore: use less params and other improvements

* chore: name

* chore: improvement

* fix: type err

* Typo fix

* chore: fix type err

* refactor: more readable

* refactor: improve code

* fix: conflicts

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2024-10-02 10:26:25 -04:00
a99e285d76 fix: fix ui overflow on phone for calendar settings (#16661)
* fix: fix ui overflow on phone for calendar settings

* fix: overflow issue with long emails

* Update CalendarSwitch.tsx

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-10-02 12:24:25 +00:00
Lauris Skraucis bdea436f8b docs: v2 refresh tokens and slots endpoints swagger (#16788)
* docs: oauth refresh endpoint

* docs: slots reserve and delete

* docs: get slots

* swagger

* fix: reserving slot

* Revert "fix: reserving slot"

This reverts commit 8a3ce8464d5dbf67e58da6d3b1aeb51df97240cb.

* Update oauth-flow.controller.ts
2024-10-02 08:44:05 +00:00
Alex van Andel 182d0fa228 fix: Better defaulting to NEXT_PUBLIC_LOGGER_LEVEL=4 (#16900)
* fix: Better defaulting to NEXT_PUBLIC_LOGGER_LEVEL=4

* chore: Minor fixes to prettyLogTemplate (as it wasnt very pretty)
2024-10-01 17:38:02 +00:00
Alex van Andel 0d7f560c43 chore: improve instrumentation deps (#16899)
* Remove Sentry from direct import in instrumentation file

* chore: Exclude Sentry configs when not needed
2024-10-01 17:52:13 +01:00
875 changed files with 42007 additions and 6340 deletions
+1 -1
View File
@@ -168,13 +168,13 @@ NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
STRIPE_TEAM_MONTHLY_PRICE_ID=
STRIPE_TEAM_PRODUCT_ID=
STRIPE_ORG_MONTHLY_PRICE_ID=
STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_SECRET_APPS=
STRIPE_PRIVATE_KEY=
STRIPE_CLIENT_ID=
# Use for internal Public API Keys and optional
API_KEY_PREFIX=cal_
# ***********************************************************************************************************
+1 -1
View File
@@ -12,7 +12,7 @@
## Mandatory Tasks (DO NOT REMOVE)
- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com/docs). If N/A, write N/A here and check the checkbox.
- [ ] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox.
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works.
## How should this be tested?
+41
View File
@@ -0,0 +1,41 @@
# This is just to test this file
name: Build
on:
workflow_call:
jobs:
build:
name: Build Docs
permissions:
contents: read
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- name: Cache Docs build
uses: buildjet/cache@v4
id: cache-docs-build
env:
cache-name: docs-build
key-1: ${{ hashFiles('yarn.lock') }}
key-2: ${{ hashFiles('docs/**.*', '!**/node_modules') }}
key-3: ${{ github.event.pull_request.number || github.ref }}
key-4: ${{ github.sha }}
with:
path: |
**/docs/**
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
- name: Run build
working-directory: docs
run: |
export NODE_OPTIONS="--max_old_space_size=8192"
if [ ${{ steps.cache-docs-build.outputs.cache-hit }} == 'true' ]; then
echo "Cache hit for Docs build. Skipping build."
else
npm install -g mintlify
mintlify dev &
sleep 5 # Let it run for 5 seconds
kill $!
fi
shell: bash
+9 -1
View File
@@ -33,7 +33,7 @@ jobs:
with:
filters: |
has-files-requiring-all-checks:
- "!(**.md|.github/CODEOWNERS)"
- "!(**.md|.github/CODEOWNERS|docs/**|apps/web/public/static/locales/**/common.json)"
- name: Get Latest Commit SHA
id: get_sha
run: |
@@ -139,6 +139,13 @@ jobs:
uses: ./.github/workflows/atoms-production-build.yml
secrets: inherit
build-docs:
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/docs-build.yml
secrets: inherit
build:
name: Production builds
needs: [changes, check-label, deps]
@@ -225,6 +232,7 @@ jobs:
build-api-v1,
build-api-v2,
build-atoms,
build-docs,
e2e,
e2e-api-v2,
e2e-embed,
@@ -39,7 +39,7 @@ env:
jobs:
build:
name: Web App
name: Build Web App
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 30
steps:
+3 -2
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { _AttendeeModel as Attendee } from "@calcom/prisma/zod";
import { timeZone } from "~/lib/validations/shared/timeZone";
@@ -14,7 +15,7 @@ export const schemaAttendeeBaseBodyParams = Attendee.pick({
const schemaAttendeeCreateParams = z
.object({
bookingId: z.number().int(),
email: z.string().email(),
email: emailSchema,
name: z.string(),
timeZone: timeZone,
})
@@ -23,7 +24,7 @@ const schemaAttendeeCreateParams = z
const schemaAttendeeEditParams = z
.object({
name: z.string().optional(),
email: z.string().email().optional(),
email: emailSchema.optional(),
timeZone: timeZone.optional(),
})
.strict();
+1
View File
@@ -73,6 +73,7 @@ export const schemaBookingReadPublic = Booking.extend({
attendees: z
.array(
_AttendeeModel.pick({
id: true,
email: true,
name: true,
timeZone: true,
@@ -1,16 +1,18 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryAttendeeEmail = baseApiParams.extend({
attendeeEmail: z.string().email(),
attendeeEmail: emailSchema,
});
export const schemaQuerySingleOrMultipleAttendeeEmails = z.object({
attendeeEmail: z.union([z.string().email(), z.array(z.string().email())]).optional(),
attendeeEmail: z.union([emailSchema, z.array(emailSchema)]).optional(),
});
export const withValidQueryAttendeeEmail = withValidation({
@@ -1,16 +1,18 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryUserEmail = baseApiParams.extend({
email: z.string().email(),
email: emailSchema,
});
export const schemaQuerySingleOrMultipleUserEmails = z.object({
email: z.union([z.string().email(), z.array(z.string().email())]),
email: z.union([emailSchema, z.array(emailSchema)]),
});
export const withValidQueryUserEmail = withValidation({
+1
View File
@@ -11,6 +11,7 @@ export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true })
smsLockState: true,
smsLockReviewedByAdmin: true,
bookingLimits: true,
includeManagedEventsInLimits: true,
});
const schemaTeamRequiredParams = z.object({
+3 -2
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import { _UserModel as User } from "@calcom/prisma/zod";
import { iso8601 } from "@calcom/prisma/zod-utils";
@@ -92,7 +93,7 @@ export const schemaUserBaseBodyParams = User.pick({
// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value
// for example making weekStart only accept weekdays as input
const schemaUserEditParams = z.object({
email: z.string().email().toLowerCase(),
email: emailSchema.toLowerCase(),
username: usernameSchema,
weekStart: z.nativeEnum(weekdays).optional(),
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
@@ -115,7 +116,7 @@ const schemaUserEditParams = z.object({
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
const schemaUserCreateParams = z.object({
email: z.string().email().toLowerCase(),
email: emailSchema.toLowerCase(),
username: usernameSchema,
weekStart: z.nativeEnum(weekdays).optional(),
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
+1 -1
View File
@@ -33,7 +33,7 @@
"@sentry/nextjs": "^8.8.0",
"bcryptjs": "^2.4.3",
"memory-cache": "^0.2.0",
"next": "^13.5.4",
"next": "^13.5.6",
"next-api-middleware": "^1.0.1",
"next-axiom": "^0.17.0",
"next-swagger-doc": "^0.3.6",
+1 -1
View File
@@ -28,7 +28,7 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.44",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.45",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
-2
View File
@@ -3,8 +3,6 @@ import { Controller, Get, Version, VERSION_NEUTRAL } from "@nestjs/common";
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
@Controller()
@DocsTags("Health - development only")
@DocsExcludeController(getEnv("NODE_ENV") === "production")
export class AppController {
@Get("health")
@Version(VERSION_NEUTRAL)
@@ -14,7 +14,7 @@ export class BookingsRepository_2024_08_13 {
});
}
async getByIdsWithAttendeesAndUser(ids: number[]) {
async getByIdsWithAttendeesAndUserAndEvent(ids: number[]) {
return this.dbRead.prisma.booking.findMany({
where: {
id: {
@@ -24,6 +24,7 @@ export class BookingsRepository_2024_08_13 {
include: {
attendees: true,
user: true,
eventType: true,
},
});
}
@@ -36,7 +37,7 @@ export class BookingsRepository_2024_08_13 {
});
}
async getByIdWithAttendeesAndUser(id: number) {
async getByIdWithAttendeesAndUserAndEvent(id: number) {
return this.dbRead.prisma.booking.findUnique({
where: {
id,
@@ -44,11 +45,12 @@ export class BookingsRepository_2024_08_13 {
include: {
attendees: true,
user: true,
eventType: true,
},
});
}
async getByUidWithAttendeesAndUser(uid: string) {
async getByUidWithAttendeesAndUserAndEvent(uid: string) {
return this.dbRead.prisma.booking.findUnique({
where: {
uid,
@@ -56,11 +58,12 @@ export class BookingsRepository_2024_08_13 {
include: {
attendees: true,
user: true,
eventType: true,
},
});
}
async getRecurringByUidWithAttendeesAndUser(uid: string) {
async getRecurringByUidWithAttendeesAndUserAndEvent(uid: string) {
return this.dbRead.prisma.booking.findMany({
where: {
recurringEventId: uid,
@@ -68,6 +71,7 @@ export class BookingsRepository_2024_08_13 {
include: {
attendees: true,
user: true,
eventType: true,
},
});
}
@@ -1,8 +1,6 @@
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output";
import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output";
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
@@ -36,6 +34,7 @@ import { User } from "@prisma/client";
import { Request } from "express";
import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { GetBookingOutput_2024_08_13, GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
import {
CreateBookingInputPipe,
CreateBookingInput,
@@ -1,7 +1,6 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
@@ -29,6 +28,7 @@ import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RecurringBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
} from "@calcom/platform-types";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
@@ -2,8 +2,6 @@ import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output";
import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output";
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
@@ -30,6 +28,7 @@ import {
VERSION_2024_08_13,
X_CAL_CLIENT_ID,
} from "@calcom/platform-constants";
import { GetBookingOutput_2024_08_13, GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
@@ -58,6 +57,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
let user: User;
let eventTypeId: number;
const eventTypeSlug = "peer-coding";
let recurringEventTypeId: number;
let createdBooking: BookingOutput_2024_08_13;
@@ -100,7 +100,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
{ title: "peer coding", slug: eventTypeSlug, length: 60 },
user.id
);
eventTypeId = event.id;
@@ -184,7 +184,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
meetingUrl: "https://meet.google.com/abc-def-ghi",
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
};
return request(app.getHttpServer())
@@ -208,14 +211,24 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString());
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(eventTypeId);
expect(data.eventType).toEqual({
id: eventTypeId,
slug: eventTypeSlug,
});
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(data.meetingUrl).toEqual(body.meetingUrl);
expect(data.location).toEqual(body.location);
expect(data.meetingUrl).toEqual(body.location);
expect(data.absentHost).toEqual(false);
expect(data.bookingFieldsResponses).toEqual({
name: body.attendee.name,
email: body.attendee.email,
...body.bookingFieldsResponses,
});
createdBooking = data;
} else {
throw new Error(
@@ -235,7 +248,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
meetingUrl: "https://meet.google.com/abc-def-ghi",
location: "https://meet.google.com/abc-def-ghi",
};
return request(app.getHttpServer())
@@ -268,7 +281,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
language: body.attendee.language,
absent: false,
});
expect(firstBooking.meetingUrl).toEqual(body.meetingUrl);
expect(firstBooking.location).toEqual(body.location);
expect(firstBooking.recurringBookingUid).toBeDefined();
expect(firstBooking.absentHost).toEqual(false);
@@ -288,7 +301,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
language: body.attendee.language,
absent: false,
});
expect(secondBooking.meetingUrl).toEqual(body.meetingUrl);
expect(secondBooking.location).toEqual(body.location);
expect(secondBooking.absentHost).toEqual(false);
const thirdBooking = data[2];
@@ -307,7 +320,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
language: body.attendee.language,
absent: false,
});
expect(thirdBooking.meetingUrl).toEqual(body.meetingUrl);
expect(thirdBooking.location).toEqual(body.location);
expect(thirdBooking.absentHost).toEqual(false);
createdRecurringBooking = data;
@@ -343,7 +356,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.duration).toEqual(createdBooking.duration);
expect(data.eventTypeId).toEqual(createdBooking.eventTypeId);
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
expect(data.meetingUrl).toEqual(createdBooking.meetingUrl);
expect(data.location).toEqual(createdBooking.location);
expect(data.absentHost).toEqual(createdBooking.absentHost);
} else {
throw new Error(
@@ -377,7 +390,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid);
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
expect(data.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl);
expect(data.location).toEqual(createdRecurringBooking[0].location);
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
} else {
throw new Error(
@@ -414,7 +427,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
expect(firstBooking.recurringBookingUid).toEqual(recurringBookingUid);
expect(firstBooking.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
expect(firstBooking.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl);
expect(firstBooking.location).toEqual(createdRecurringBooking[0].location);
expect(firstBooking.absentHost).toEqual(createdRecurringBooking[0].absentHost);
const secondBooking = data[1];
@@ -428,7 +441,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.eventTypeId).toEqual(createdRecurringBooking[1].eventTypeId);
expect(secondBooking.recurringBookingUid).toEqual(recurringBookingUid);
expect(secondBooking.attendees[0]).toEqual(createdRecurringBooking[1].attendees[0]);
expect(secondBooking.meetingUrl).toEqual(createdRecurringBooking[1].meetingUrl);
expect(secondBooking.location).toEqual(createdRecurringBooking[1].location);
expect(secondBooking.absentHost).toEqual(createdRecurringBooking[1].absentHost);
const thirdBooking = data[2];
@@ -442,7 +455,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(thirdBooking.eventTypeId).toEqual(createdRecurringBooking[2].eventTypeId);
expect(thirdBooking.recurringBookingUid).toEqual(recurringBookingUid);
expect(thirdBooking.attendees[0]).toEqual(createdRecurringBooking[2].attendees[0]);
expect(thirdBooking.meetingUrl).toEqual(createdRecurringBooking[2].meetingUrl);
expect(thirdBooking.location).toEqual(createdRecurringBooking[2].location);
expect(thirdBooking.absentHost).toEqual(createdRecurringBooking[2].absentHost);
createdRecurringBooking = data;
@@ -764,7 +777,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.duration).toEqual(createdBooking.duration);
expect(data.eventTypeId).toEqual(createdBooking.eventTypeId);
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
expect(data.meetingUrl).toEqual(createdBooking.meetingUrl);
expect(data.location).toEqual(createdBooking.location);
expect(data.absentHost).toEqual(createdBooking.absentHost);
rescheduledBooking = data;
@@ -815,7 +828,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid);
expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
expect(data.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl);
expect(data.location).toEqual(createdRecurringBooking[0].location);
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid);
@@ -885,7 +898,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.duration).toEqual(booking.duration);
expect(data.eventTypeId).toEqual(booking.eventTypeId);
expect(data.attendees[0]).toEqual(booking.attendees[0]);
expect(data.meetingUrl).toEqual(booking.meetingUrl);
expect(data.location).toEqual(booking.location);
});
});
@@ -918,7 +931,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.eventTypeId).toEqual(booking.eventTypeId);
expect(data.attendees[0].absent).toEqual(true);
expect(data.absentHost).toEqual(booking.absentHost);
expect(data.meetingUrl).toEqual(booking.meetingUrl);
expect(data.location).toEqual(booking.location);
});
});
});
@@ -955,7 +968,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.duration).toEqual(rescheduledBooking.duration);
expect(data.eventTypeId).toEqual(rescheduledBooking.eventTypeId);
expect(data.attendees[0]).toEqual(rescheduledBooking.attendees[0]);
expect(data.meetingUrl).toEqual(rescheduledBooking.meetingUrl);
expect(data.location).toEqual(rescheduledBooking.location);
expect(data.absentHost).toEqual(rescheduledBooking.absentHost);
const cancelledBooking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid);
@@ -1005,10 +1018,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
return (
!Array.isArray(data) &&
@@ -1019,10 +1028,303 @@ describe("Bookings Endpoints 2024-08-13", () => {
);
}
function responseDataIsRecurringBooking(data: any): data is RecurringBookingOutput_2024_08_13[] {
return Array.isArray(data);
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(user.email);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
await app.close();
});
});
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
function responseDataIsRecurringBooking(data: any): data is RecurringBookingOutput_2024_08_13[] {
return Array.isArray(data);
}
describe("Recurring bookings", () => {
let app: INestApplication;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
const userEmail = "bookings-controller-e2e@api.com";
let user: User;
const maxRecurrenceCount = 3;
let recurringEventTypeId: number;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
)
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const recurringEvent = await eventTypesRepositoryFixture.create(
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
{
title: "peer coding recurring",
slug: "peer-coding-recurring",
length: 60,
recurringEvent: { freq: 2, count: maxRecurrenceCount, interval: 1 },
},
user.id
);
recurringEventTypeId = recurringEvent.id;
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it("should not create recurring booking with recurrenceCount larger than event type recurrence count", async () => {
const recurrenceCount = 1000;
const body: CreateRecurringBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
eventTypeId: recurringEventTypeId,
attendee: {
name: "Mr Proper Recurring",
email: "mr_proper_recurring@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
recurrenceCount,
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
it("should create a recurring booking with recurrenceCount smaller than event type recurrence count", async () => {
const recurrenceCount = maxRecurrenceCount - 1;
const body: CreateRecurringBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
eventTypeId: recurringEventTypeId,
attendee: {
name: "Mr Proper Recurring",
email: "mr_proper_recurring@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
recurrenceCount,
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(maxRecurrenceCount - 1);
const firstBooking = data[0];
expect(firstBooking.id).toBeDefined();
expect(firstBooking.uid).toBeDefined();
expect(firstBooking.hosts[0].id).toEqual(user.id);
expect(firstBooking.status).toEqual("accepted");
expect(firstBooking.start).toEqual(new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString());
expect(firstBooking.end).toEqual(new Date(Date.UTC(2030, 1, 4, 14, 0, 0)).toISOString());
expect(firstBooking.duration).toEqual(60);
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(firstBooking.location).toEqual(body.location);
expect(firstBooking.recurringBookingUid).toBeDefined();
expect(firstBooking.absentHost).toEqual(false);
const secondBooking = data[1];
expect(secondBooking.id).toBeDefined();
expect(secondBooking.uid).toBeDefined();
expect(secondBooking.hosts[0].id).toEqual(user.id);
expect(secondBooking.status).toEqual("accepted");
expect(secondBooking.start).toEqual(new Date(Date.UTC(2030, 1, 11, 13, 0, 0)).toISOString());
expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString());
expect(secondBooking.duration).toEqual(60);
expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(secondBooking.recurringBookingUid).toBeDefined();
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(secondBooking.location).toEqual(body.location);
expect(secondBooking.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
});
it("should create a recurring booking with recurrenceCount equal to event type recurrence count", async () => {
const recurrenceCount = maxRecurrenceCount;
const body: CreateRecurringBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
eventTypeId: recurringEventTypeId,
attendee: {
name: "Mr Proper Recurring",
email: "mr_proper_recurring@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
recurrenceCount,
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(maxRecurrenceCount);
const firstBooking = data[0];
expect(firstBooking.id).toBeDefined();
expect(firstBooking.uid).toBeDefined();
expect(firstBooking.hosts[0].id).toEqual(user.id);
expect(firstBooking.status).toEqual("accepted");
expect(firstBooking.start).toEqual(new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString());
expect(firstBooking.end).toEqual(new Date(Date.UTC(2030, 1, 4, 14, 0, 0)).toISOString());
expect(firstBooking.duration).toEqual(60);
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(firstBooking.location).toEqual(body.location);
expect(firstBooking.meetingUrl).toEqual(body.location);
expect(firstBooking.recurringBookingUid).toBeDefined();
expect(firstBooking.absentHost).toEqual(false);
const secondBooking = data[1];
expect(secondBooking.id).toBeDefined();
expect(secondBooking.uid).toBeDefined();
expect(secondBooking.hosts[0].id).toEqual(user.id);
expect(secondBooking.status).toEqual("accepted");
expect(secondBooking.start).toEqual(new Date(Date.UTC(2030, 1, 11, 13, 0, 0)).toISOString());
expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString());
expect(secondBooking.duration).toEqual(60);
expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(secondBooking.recurringBookingUid).toBeDefined();
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(secondBooking.location).toEqual(body.location);
expect(secondBooking.absentHost).toEqual(false);
const thirdBooking = data[2];
expect(thirdBooking.id).toBeDefined();
expect(thirdBooking.uid).toBeDefined();
expect(thirdBooking.hosts[0].id).toEqual(user.id);
expect(thirdBooking.status).toEqual("accepted");
expect(thirdBooking.start).toEqual(new Date(Date.UTC(2030, 1, 18, 13, 0, 0)).toISOString());
expect(thirdBooking.end).toEqual(new Date(Date.UTC(2030, 1, 18, 14, 0, 0)).toISOString());
expect(thirdBooking.duration).toEqual(60);
expect(thirdBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(thirdBooking.recurringBookingUid).toBeDefined();
expect(thirdBooking.attendees[0]).toEqual({
name: body.attendee.name,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(thirdBooking.location).toEqual(body.location);
expect(thirdBooking.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
});
afterEach(async () => {
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -73,7 +73,9 @@ export class BookingsService_2024_08_13 {
const bookingRequest = await this.inputService.createBookingRequest(request, body);
const booking = await handleInstantMeeting(bookingRequest);
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.bookingId);
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(
booking.bookingId
);
if (!databaseBooking) {
throw new Error(`Booking with id=${booking.bookingId} was not found in the database`);
}
@@ -94,7 +96,7 @@ export class BookingsService_2024_08_13 {
throw new Error("Recurring booking was not created");
}
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUser(uid);
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUserAndEvent(uid);
return this.outputService.getOutputRecurringBookings(recurringBooking);
}
@@ -106,7 +108,7 @@ export class BookingsService_2024_08_13 {
throw new Error("Booking was not created");
}
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.id);
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(booking.id);
if (!databaseBooking) {
throw new Error(`Booking with id=${booking.id} was not found in the database`);
}
@@ -115,7 +117,7 @@ export class BookingsService_2024_08_13 {
}
async getBooking(uid: string) {
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUser(uid);
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(uid);
if (booking) {
const isRecurring = !!booking.recurringEventId;
@@ -125,7 +127,7 @@ export class BookingsService_2024_08_13 {
return this.outputService.getOutputBooking(booking);
}
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUser(uid);
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUserAndEvent(uid);
if (!recurringBooking.length) {
throw new NotFoundException(`Booking with uid=${uid} was not found in the database`);
}
@@ -149,7 +151,7 @@ export class BookingsService_2024_08_13 {
// note(Lauris): fetchedBookings don't have attendees information and responses and i don't want to add them to the handler query,
// because its used elsewhere in code that does not need that information, so i get ids, fetch bookings and then return them formatted in same order as ids.
const ids = fetchedBookings.bookings.map((booking) => booking.id);
const bookings = await this.bookingsRepository.getByIdsWithAttendeesAndUser(ids);
const bookings = await this.bookingsRepository.getByIdsWithAttendeesAndUserAndEvent(ids);
const bookingMap = new Map(bookings.map((booking) => [booking.id, booking]));
const orderedBookings = ids.map((id) => bookingMap.get(id));
@@ -162,6 +164,7 @@ export class BookingsService_2024_08_13 {
const formatted = {
...booking,
eventType: booking.eventType,
eventTypeId: booking.eventTypeId,
startTime: new Date(booking.startTime),
endTime: new Date(booking.endTime),
@@ -191,7 +194,7 @@ export class BookingsService_2024_08_13 {
throw new Error("Booking was not created");
}
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.id);
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(booking.id);
if (!databaseBooking) {
throw new Error(`Booking with id=${booking.id} was not found in the database`);
}
@@ -226,7 +229,7 @@ export class BookingsService_2024_08_13 {
userId: bookingOwnerId,
});
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUser(bookingUid);
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
if (!booking) {
throw new Error(`Booking with uid=${bookingUid} was not found in the database`);
@@ -5,7 +5,7 @@ import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { Injectable, NotFoundException } from "@nestjs/common";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Request } from "express";
@@ -57,9 +57,9 @@ export enum Frequency {
const recurringEventSchema = z.object({
dtstart: z.string().optional(),
interval: z.number().int().optional(),
count: z.number().int().optional(),
freq: z.nativeEnum(Frequency).optional(),
interval: z.number().int(),
count: z.number().int(),
freq: z.nativeEnum(Frequency),
until: z.string().optional(),
});
@@ -89,7 +89,7 @@ export class InputBookingsService_2024_08_13 {
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
: DEFAULT_PLATFORM_PARAMS;
const location = request.body.meetingUrl;
const location = request.body.location || request.body.meetingUrl;
Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled };
@@ -147,7 +147,7 @@ export class InputBookingsService_2024_08_13 {
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
: DEFAULT_PLATFORM_PARAMS;
const location = request.body.meetingUrl;
const location = request.body.location || request.body.meetingUrl;
Object.assign(newRequest, {
userId,
...oAuthParams,
@@ -175,14 +175,16 @@ export class InputBookingsService_2024_08_13 {
const occurrence = recurringEventSchema.parse(eventType.recurringEvent);
const repeatsEvery = occurrence.interval;
const repeatsTimes = occurrence.count;
if (inputBooking.recurrenceCount && inputBooking.recurrenceCount > occurrence.count) {
throw new BadRequestException(
"Provided recurrence count is higher than the event type's recurring event count."
);
}
const repeatsTimes = inputBooking.recurrenceCount || occurrence.count;
// note(Lauris): timeBetween 0=yearly, 1=monthly and 2=weekly
const timeBetween = occurrence.freq;
if (!repeatsTimes) {
throw new Error("Repeats times is required");
}
const events = [];
const recurringEventId = uuidv4();
@@ -257,7 +259,7 @@ export class InputBookingsService_2024_08_13 {
}
async transformInputRescheduleBooking(bookingUid: string, inputBooking: RescheduleBookingInput_2024_08_13) {
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUser(bookingUid);
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
}
@@ -390,7 +392,9 @@ export class InputBookingsService_2024_08_13 {
async transformInputCancelBooking(bookingUid: string, inputBooking: CancelBookingInput_2024_08_13) {
let allRemainingBookings = false;
let uid = bookingUid;
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUser(bookingUid);
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUserAndEvent(
bookingUid
);
if (recurringBooking.length) {
// note(Lauirs): this means that bookingUid is equal to recurringEventId on individual bookings of recurring one aka main recurring event
@@ -7,14 +7,20 @@ import { z } from "zod";
import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types";
import { Booking } from "@calcom/prisma/client";
export const bookingResponsesSchema = z.object({
email: z.string(),
name: z.string(),
guests: z.array(z.string()).optional(),
rescheduledReason: z.string().optional(),
});
export const bookingResponsesSchema = z
.object({
email: z.string(),
name: z.string(),
guests: z.array(z.string()).optional(),
rescheduledReason: z.string().optional(),
})
.passthrough();
type DatabaseBooking = Booking & {
eventType: {
id: number;
slug: string;
} | null;
attendees: {
name: string;
email: string;
@@ -38,6 +44,8 @@ export class OutputBookingsService_2024_08_13 {
const booking = {
id: databaseBooking.id,
uid: databaseBooking.uid,
title: databaseBooking.title,
description: databaseBooking.description,
hosts: [databaseBooking.user],
status: databaseBooking.status.toLowerCase(),
cancellationReason: databaseBooking.cancellationReason || undefined,
@@ -46,6 +54,8 @@ export class OutputBookingsService_2024_08_13 {
start: databaseBooking.startTime,
end: databaseBooking.endTime,
duration,
eventType: databaseBooking.eventType,
// note(Lauris): eventTypeId is deprecated
eventTypeId: databaseBooking.eventTypeId,
attendees: databaseBooking.attendees.map((attendee) => ({
name: attendee.name,
@@ -55,18 +65,22 @@ export class OutputBookingsService_2024_08_13 {
absent: !!attendee.noShow,
})),
guests: bookingResponses.guests,
location: databaseBooking.location,
// note(Lauris): meetingUrl is deprecated
meetingUrl: databaseBooking.location,
absentHost: !!databaseBooking.noShowHost,
};
return plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
bookingTransformed.bookingFieldsResponses = bookingResponses;
return bookingTransformed;
}
async getOutputRecurringBookings(databaseBookings: DatabaseBooking[]) {
const transformed = [];
for (const booking of databaseBookings) {
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.id);
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(booking.id);
if (!databaseBooking) {
throw new Error(`Booking with id=${booking.id} was not found in the database`);
}
@@ -87,6 +101,8 @@ export class OutputBookingsService_2024_08_13 {
const booking = {
id: databaseBooking.id,
uid: databaseBooking.uid,
title: databaseBooking.title,
description: databaseBooking.description,
hosts: [databaseBooking.user],
status: databaseBooking.status.toLowerCase(),
cancellationReason: databaseBooking.cancellationReason || undefined,
@@ -95,6 +111,8 @@ export class OutputBookingsService_2024_08_13 {
start: databaseBooking.startTime,
end: databaseBooking.endTime,
duration,
eventType: databaseBooking.eventType,
// note(Lauris): eventTypeId is deprecated
eventTypeId: databaseBooking.eventTypeId,
attendees: databaseBooking.attendees.map((attendee) => ({
name: attendee.name,
@@ -104,9 +122,12 @@ export class OutputBookingsService_2024_08_13 {
absent: !!attendee.noShow,
})),
guests: bookingResponses.guests,
location: databaseBooking.location,
// note(Lauris): meetingUrl is deprecated
meetingUrl: databaseBooking.location,
recurringBookingUid: databaseBooking.recurringEventId,
absentHost: !!databaseBooking.noShowHost,
bookingFieldsResponses: databaseBooking.responses,
};
return plainToClass(RecurringBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
+3
View File
@@ -21,6 +21,9 @@ export type Environment = {
API_KEY_PREFIX: string;
DOCS_URL: string;
RATE_LIMIT_DEFAULT_TTL_MS: number;
RATE_LIMIT_DEFAULT_LIMIT_API_KEY: number;
RATE_LIMIT_DEFAULT_LIMIT_OAUTH_CLIENT: number;
RATE_LIMIT_DEFAULT_LIMIT_ACCESS_TOKEN: number;
RATE_LIMIT_DEFAULT_LIMIT: number;
RATE_LIMIT_DEFAULT_BLOCK_DURATION_MS: number;
};
+52 -13
View File
@@ -22,18 +22,23 @@ const rateLimitSchema = z.object({
ttl: z.number(),
blockDuration: z.number(),
});
type RateLimitType = z.infer<typeof rateLimitSchema>;
const rateLimitsSchema = z.array(rateLimitSchema);
const sixtySecondsMs = 60 * 1000;
@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
private logger = new Logger("CustomThrottlerGuard");
private defaultTttl = Number(getEnv("RATE_LIMIT_DEFAULT_TTL_MS", 60 * 1000));
private defaultTttl = Number(getEnv("RATE_LIMIT_DEFAULT_TTL_MS", sixtySecondsMs));
private defaultLimitApiKey = Number(getEnv("RATE_LIMIT_DEFAULT_LIMIT_API_KEY", 120));
private defaultLimitOAuthClient = Number(getEnv("RATE_LIMIT_DEFAULT_LIMIT_OAUTH_CLIENT", 500));
private defaultLimitAccessToken = Number(getEnv("RATE_LIMIT_DEFAULT_LIMIT_ACCESS_TOKEN", 500));
private defaultLimit = Number(getEnv("RATE_LIMIT_DEFAULT_LIMIT", 120));
private defaultBlockDuration = Number(getEnv("RATE_LIMIT_DEFAULT_BLOCK_DURATION_MS", 60 * 1000));
private defaultBlockDuration = Number(getEnv("RATE_LIMIT_DEFAULT_BLOCK_DURATION_MS", sixtySecondsMs));
constructor(
options: ThrottlerModuleOptions,
@@ -51,6 +56,11 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();
const tracker = await this.getTracker(request);
this.logger.log(
`Tracker "${tracker}" generated based on: Bearer token "${request.get(
"Authorization"
)}", OAuth client ID "${request.get(X_CAL_CLIENT_ID)}" and IP "${request.ip}"`
);
if (tracker.startsWith("api_key_")) {
return this.handleApiKeyRequest(tracker, response);
@@ -78,7 +88,10 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
}
private async handleNonApiKeyRequest(tracker: string, response: Response): Promise<boolean> {
const rateLimit = this.getDefaultRateLimit();
const rateLimit = this.getDefaultRateLimit(tracker);
this.logger.log(`Tracker "${tracker}" uses default rate limits because it is not tracking api key:
${JSON.stringify(rateLimit, null, 2)}
`);
const { isBlocked } = await this.incrementRateLimit(tracker, rateLimit, response);
if (isBlocked) {
@@ -88,17 +101,25 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
return true;
}
private getDefaultRateLimit() {
private getDefaultRateLimit(tracker: string) {
return {
name: "default",
limit: this.getDefaultLimit(),
limit: this.getDefaultLimit(tracker),
ttl: this.getDefaultTtl(),
blockDuration: this.getDefaultBlockDuration(),
};
}
getDefaultLimit() {
return this.defaultLimit;
getDefaultLimit(tracker: string) {
if (tracker.startsWith("api_key_")) {
return this.defaultLimitApiKey;
} else if (tracker.startsWith("oauth_client_")) {
return this.defaultLimitOAuthClient;
} else if (tracker.startsWith("access_token_")) {
return this.defaultLimitAccessToken;
} else {
return this.defaultLimit;
}
}
getDefaultTtl() {
@@ -114,6 +135,9 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
const cachedRateLimits = await this.storageService.redis.get(cacheKey);
if (cachedRateLimits) {
this.logger.log(`Tracker "${tracker}" rate limits retrieved from redis cache:
${cachedRateLimits}
`);
return rateLimitsSchema.parse(JSON.parse(cachedRateLimits));
}
@@ -133,11 +157,18 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
select: { name: true, limit: true, ttl: true, blockDuration: true },
});
if (!rateLimits || rateLimits.length === 0) {
rateLimits = [this.getDefaultRateLimit()];
if (rateLimits) {
this.logger.log(`Tracker "${tracker}" rate limits retrieved from database:
${JSON.stringify(rateLimits, null, 2)}`);
}
await this.storageService.redis.setex(cacheKey, 3600, JSON.stringify(rateLimits));
if (!rateLimits || rateLimits.length === 0) {
rateLimits = [this.getDefaultRateLimit(tracker)];
this.logger.log(`Tracker "${tracker}" rate limits not found in database. Using default rate limits:
${JSON.stringify(rateLimits, null, 2)}`);
}
await this.storageService.redis.set(cacheKey, JSON.stringify(rateLimits), "EX", 3600);
return rateLimits;
}
@@ -163,7 +194,15 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
);
response.setHeader(`X-RateLimit-Reset-${nameFirstUpper}`, timeToBlockExpire || timeToExpire);
this.logger.log(`Rate limit for ${tracker} incremented. Remaining: ${limit - totalHits}`);
this.logger.log(
`Tracker "${tracker}" rate limit "${name}" incremented. isBlocked ${isBlocked}, totalHits ${totalHits}, timeToExpire ${timeToExpire}, timeToBlockExpire ${timeToBlockExpire}`
);
this.logger.log(
`Tracker "${tracker}" rate limit "${name}" response headers:
X-RateLimit-Limit-${nameFirstUpper}: ${limit},
X-RateLimit-Remaining-${nameFirstUpper}: ${timeToBlockExpire ? 0 : Math.max(0, limit - totalHits)},
X-RateLimit-Reset-${nameFirstUpper}: ${timeToBlockExpire || timeToExpire}`
);
return { isBlocked };
}
@@ -0,0 +1,22 @@
import { ConferencingController } from "@/modules/conferencing/controllers/conferencing.controller";
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [
ConferencingService,
ConferencingRepository,
GoogleMeetService,
CredentialsRepository,
UsersRepository,
],
exports: [],
controllers: [ConferencingController],
})
export class ConferencingModule {}
@@ -0,0 +1,137 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { ConferencingAppsOutputDto } from "@/modules/conferencing/outputs/get-conferencing-apps.output";
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 { User } from "@prisma/client";
import * as request from "supertest";
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import {
ERROR_STATUS,
GOOGLE_CALENDAR_ID,
GOOGLE_CALENDAR_TYPE,
GOOGLE_MEET,
GOOGLE_MEET_TYPE,
SUCCESS_STATUS,
} from "@calcom/platform-constants";
import { ApiErrorResponse, ApiSuccessResponse } from "@calcom/platform-types";
describe("Conferencing Endpoints", () => {
describe("conferencing controller e2e tests", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let credentialsRepositoryFixture: CredentialsRepositoryFixture;
const userEmail = "conferencing-controller-user-e2e@api.com";
let user: User;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should get all the conferencing apps of the auth user", async () => {
return request(app.getHttpServer())
.get(`/v2/conferencing`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ConferencingAppsOutputDto[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toEqual([]);
});
});
it("should fail to connect google meet if google calendar is not connected ", async () => {
return request(app.getHttpServer())
.post(`/v2/conferencing/google-meet/connect`)
.expect(400)
.then(async () => {
await credentialsRepositoryFixture.create(GOOGLE_CALENDAR_TYPE, {}, user.id, GOOGLE_CALENDAR_ID);
});
});
it("should connect google meet if google calendar is connected ", async () => {
return request(app.getHttpServer())
.post(`/v2/conferencing/google-meet/connect`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ConferencingAppsOutputDto[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
});
});
it("should set google meet as default conferencing app", async () => {
return request(app.getHttpServer())
.post(`/v2/conferencing/google-meet/default`)
.expect(200)
.then(async () => {
const updatedUser = await userRepositoryFixture.get(user.id);
expect(updatedUser).toBeDefined();
if (updatedUser) {
const metadata = updatedUser.metadata as { defaultConferencingApp?: { appSlug?: string } };
expect(metadata?.defaultConferencingApp?.appSlug).toEqual(GOOGLE_MEET);
}
});
});
it("should get all the conferencing apps of the auth user, and contain google meet", async () => {
return request(app.getHttpServer())
.get(`/v2/conferencing`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ConferencingAppsOutputDto[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const googleMeet = responseBody.data.find((app) => app.type === GOOGLE_MEET_TYPE);
expect(googleMeet?.userId).toEqual(user.id);
});
});
it("should disconnect google meet", async () => {
return request(app.getHttpServer()).delete(`/v2/conferencing/google-meet/disconnect`).expect(200);
});
it("should get all the conferencing apps of the auth user, and not contain google meet", async () => {
return request(app.getHttpServer())
.get(`/v2/conferencing`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ConferencingAppsOutputDto[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const googleMeet = responseBody.data.find((app) => app.type === GOOGLE_MEET_TYPE);
expect(googleMeet).toBeUndefined();
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await app.close();
});
});
});
@@ -0,0 +1,121 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import {
ConferencingAppsOutputResponseDto,
ConferencingAppOutputResponseDto,
ConferencingAppsOutputDto,
} from "@/modules/conferencing/outputs/get-conferencing-apps.output";
import { SetDefaultConferencingAppOutputResponseDto } from "@/modules/conferencing/outputs/set-default-conferencing-app.output";
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service";
import {
Controller,
Get,
HttpCode,
HttpStatus,
Logger,
UseGuards,
Post,
Param,
BadRequestException,
Delete,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToInstance } from "class-transformer";
import { CONFERENCING_APPS, GOOGLE_MEET, SUCCESS_STATUS } from "@calcom/platform-constants";
@Controller({
path: "/v2/conferencing",
version: API_VERSIONS_VALUES,
})
@DocsTags("Conferencing")
export class ConferencingController {
private readonly logger = new Logger("Platform Gcal Provider");
constructor(
private readonly conferencingService: ConferencingService,
private readonly googleMeetService: GoogleMeetService
) {}
@Post("/:app/connect")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Connect your conferencing application" })
async connect(
@GetUser("id") userId: number,
@Param("app") app: string
): Promise<ConferencingAppOutputResponseDto> {
switch (app) {
case GOOGLE_MEET:
const credential = await this.googleMeetService.connectGoogleMeetApp(userId);
return { status: SUCCESS_STATUS, data: plainToInstance(ConferencingAppsOutputDto, credential) };
default:
throw new BadRequestException(
"Invalid conferencing app, available apps are: ",
CONFERENCING_APPS.join(", ")
);
}
}
@Get("/")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "List your conferencing applications" })
async listConferencingApps(@GetUser("id") userId: number): Promise<ConferencingAppsOutputResponseDto> {
const conferencingApps = await this.conferencingService.getConferencingApps(userId);
const data = conferencingApps.map((conferencingApps) =>
plainToInstance(ConferencingAppsOutputDto, conferencingApps)
);
return { status: SUCCESS_STATUS, data };
}
@Post("/:app/default")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Set your default conferencing application" })
async default(
@GetUser("id") userId: number,
@Param("app") app: string
): Promise<SetDefaultConferencingAppOutputResponseDto> {
switch (app) {
case GOOGLE_MEET:
await this.googleMeetService.setDefault(userId);
return { status: SUCCESS_STATUS };
default:
throw new BadRequestException(
"Invalid conferencing app, available apps are: ",
CONFERENCING_APPS.join(", ")
);
}
}
@Delete("/:app/disconnect")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Disconnect your conferencing application" })
async disconnect(
@GetUser("id") userId: number,
@Param("app") app: string
): Promise<ConferencingAppOutputResponseDto> {
switch (app) {
case GOOGLE_MEET:
const credential = await this.googleMeetService.disconnectGoogleMeetApp(userId);
return { status: SUCCESS_STATUS, data: plainToInstance(ConferencingAppsOutputDto, credential) };
default:
throw new BadRequestException(
"Invalid conferencing app, available apps are: ",
CONFERENCING_APPS.join(", ")
);
}
}
}
@@ -0,0 +1,50 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsString, ValidateNested, IsEnum, IsNumber, IsOptional, IsBoolean } from "class-validator";
import { ERROR_STATUS, GOOGLE_MEET_TYPE, SUCCESS_STATUS } from "@calcom/platform-constants";
export class ConferencingAppsOutputDto {
@Expose()
@IsNumber()
@ApiProperty({ description: "Id of the conferencing app credentials" })
id!: number;
@ApiProperty({ example: GOOGLE_MEET_TYPE, description: "Type of conferencing app" })
@Expose()
@IsString()
type!: string;
@ApiProperty({ description: "Id of the user associated to the conferencing app" })
@Expose()
@IsNumber()
userId!: number;
@ApiProperty({ example: true, description: "Whether if the connection is working or not." })
@Expose()
@IsBoolean()
@IsOptional()
invalid?: boolean | null;
}
export class ConferencingAppsOutputResponseDto {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@Expose()
@ValidateNested()
@Type(() => ConferencingAppsOutputDto)
data!: ConferencingAppsOutputDto[];
}
export class ConferencingAppOutputResponseDto {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@Expose()
@ValidateNested()
@Type(() => ConferencingAppsOutputDto)
data!: ConferencingAppsOutputDto;
}
@@ -0,0 +1,10 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum } from "class-validator";
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
export class SetDefaultConferencingAppOutputResponseDto {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
}
@@ -0,0 +1,25 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { GOOGLE_MEET_TYPE } from "@calcom/platform-constants";
@Injectable()
export class ConferencingRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async findConferencingApps(userId: number) {
return this.dbRead.prisma.credential.findMany({
where: {
userId,
type: { endsWith: "_video" },
},
});
}
async findGoogleMeet(userId: number) {
return this.dbRead.prisma.credential.findFirst({
where: { userId, type: GOOGLE_MEET_TYPE },
});
}
}
@@ -0,0 +1,14 @@
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
import { Logger } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
@Injectable()
export class ConferencingService {
private logger = new Logger("ConferencingService");
constructor(private readonly conferencingRepository: ConferencingRepository) {}
async getConferencingApps(userId: number) {
return this.conferencingRepository.findConferencingApps(userId);
}
}
@@ -0,0 +1,71 @@
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, InternalServerErrorException, Logger } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { GOOGLE_CALENDAR_TYPE, GOOGLE_MEET_TYPE, GOOGLE_MEET } from "@calcom/platform-constants";
@Injectable()
export class GoogleMeetService {
private logger = new Logger("GoogleMeetService");
constructor(
private readonly conferencingRepository: ConferencingRepository,
private readonly credentialsRepository: CredentialsRepository,
private readonly usersRepository: UsersRepository
) {}
async connectGoogleMeetApp(userId: number) {
const googleCalendar = await this.credentialsRepository.getByTypeAndUserId(GOOGLE_CALENDAR_TYPE, userId);
if (!googleCalendar) {
throw new BadRequestException("Google Meet app requires a Google Calendar connection");
}
if (googleCalendar.invalid) {
throw new BadRequestException(
"Google Meet app requires a valid Google Calendar connection, please reconnect Google Calendar."
);
}
const googleMeet = await this.conferencingRepository.findGoogleMeet(userId);
if (googleMeet) {
throw new BadRequestException("Google Meet is already connected.");
}
const googleMeetCredential = await this.credentialsRepository.upsertAppCredential(
GOOGLE_MEET_TYPE,
{},
userId
);
return googleMeetCredential;
}
async disconnectGoogleMeetApp(userId: number) {
const googleMeet = await this.conferencingRepository.findGoogleMeet(userId);
if (!googleMeet) {
throw new BadRequestException("Google Meet is not connected.");
}
const googleMeetCredential = await this.credentialsRepository.deleteUserCredentialById(
userId,
googleMeet.id
);
return googleMeetCredential;
}
async setDefault(userId: number) {
const user = await this.usersRepository.setDefaultConferencingApp(userId, GOOGLE_MEET);
const metadata = user.metadata as { defaultConferencingApp?: { appSlug?: string } };
if (metadata?.defaultConferencingApp?.appSlug !== GOOGLE_MEET) {
throw new InternalServerErrorException("Could not set Google Meet as default conferencing app");
}
return true;
}
}
@@ -88,6 +88,12 @@ export class CredentialsRepository {
},
});
}
async deleteUserCredentialById(userId: number, credentialId: number) {
return await this.dbWrite.prisma.credential.delete({
where: { id: credentialId, userId },
});
}
}
export type CredentialsWithUserEmail = Awaited<
@@ -1,6 +1,7 @@
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
import { AtomsModule } from "@/modules/atoms/atoms.module";
import { BillingModule } from "@/modules/billing/billing.module";
import { ConferencingModule } from "@/modules/conferencing/conferencing.module";
import { DestinationCalendarsModule } from "@/modules/destination-calendars/destination-calendars.module";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
@@ -22,6 +23,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
DestinationCalendarsModule,
AtomsModule,
StripeModule,
ConferencingModule,
],
})
export class EndpointsModule implements NestModule {
@@ -52,8 +52,6 @@ Second, make sure that the logged in user has organizationId set to pass the Org
version: API_VERSIONS_VALUES,
})
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
@DocsExcludeController(getEnv("NODE_ENV") === "production")
@DocsTags("OAuth - development only")
export class OAuthClientsController {
private readonly logger = new Logger("OAuthClientController");
@@ -25,9 +25,11 @@ import {
import {
ApiTags as DocsTags,
ApiExcludeController as DocsExcludeController,
ApiExcludeEndpoint as DocsExcludeEndpoint,
ApiOperation as DocsOperation,
ApiOkResponse as DocsOkResponse,
ApiBadRequestResponse as DocsBadRequestResponse,
ApiHeader as DocsHeader,
} from "@nestjs/swagger";
import { Response as ExpressResponse } from "express";
@@ -37,8 +39,7 @@ import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
path: "/v2/oauth/:clientId",
version: API_VERSIONS_VALUES,
})
@DocsExcludeController(getEnv("NODE_ENV") === "production")
@DocsTags("OAuth - development only")
@DocsTags("OAuth")
export class OAuthFlowController {
constructor(
private readonly oauthClientRepository: OAuthClientRepository,
@@ -62,6 +63,7 @@ export class OAuthFlowController {
description:
"Bad request if the OAuth client is not found, if the redirect URI is invalid, or if the user has already authorized the client.",
})
@DocsExcludeEndpoint(getEnv("NODE_ENV") === "production")
async authorize(
@Param("clientId") clientId: string,
@Body() body: OAuthAuthorizeInput,
@@ -108,6 +110,7 @@ export class OAuthFlowController {
description:
"Bad request if the authorization code is missing, invalid, or if the client ID and secret do not match.",
})
@DocsExcludeEndpoint(getEnv("NODE_ENV") === "production")
async exchange(
@Headers("Authorization") authorization: string,
@Param("clientId") clientId: string,
@@ -138,6 +141,12 @@ export class OAuthFlowController {
@Post("/refresh")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@DocsTags("Managed users")
@DocsHeader({
name: X_CAL_SECRET_KEY,
description: "OAuth client secret key.",
required: true,
})
async refreshAccessToken(
@Param("clientId") clientId: string,
@Headers(X_CAL_SECRET_KEY) secretKey: string,
@@ -1,6 +1,8 @@
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { IsString } from "class-validator";
export class RefreshTokenInput {
@IsString()
@DocsProperty({ description: "Managed user's refresh token." })
refreshToken!: string;
}
@@ -76,4 +76,8 @@ export class UpdateOrgTeamDto {
@IsOptional()
@IsString()
readonly bookingLimits?: string;
@IsOptional()
@IsBoolean()
readonly includeManagedEventsInLimits?: boolean;
}
@@ -1,7 +1,7 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { SlotsService } from "@/modules/slots/services/slots.service";
import { Query, Body, Controller, Get, Delete, Post, Req, Res } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiTags as DocsTags, ApiCreatedResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger";
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -19,6 +19,21 @@ export class SlotsController {
constructor(private readonly slotsService: SlotsService) {}
@Post("/reserve")
@ApiCreatedResponse({
description: "Successful response returning uid of reserved slot.",
schema: {
type: "object",
properties: {
status: { type: "string", example: "success" },
data: {
type: "object",
properties: {
uid: { type: "string", example: "e2a7bcf9-cc7b-40a0-80d3-657d391775a6" },
},
},
},
},
})
@ApiOperation({ summary: "Reserve a slot" })
async reserveSlot(
@Body() body: ReserveSlotInput,
@@ -35,6 +50,15 @@ export class SlotsController {
}
@Delete("/selected-slot")
@ApiOkResponse({
description: "Response deleting reserved slot by uid.",
schema: {
type: "object",
properties: {
status: { type: "string", example: "success" },
},
},
})
@ApiOperation({ summary: "Delete a selected slot" })
async deleteSelectedSlot(
@Query() params: RemoveSelectedSlotInput,
@@ -50,6 +74,45 @@ export class SlotsController {
}
@Get("/available")
@ApiOkResponse({
description: "Available time slots retrieved successfully",
schema: {
type: "object",
properties: {
status: { type: "string", example: "success" },
data: {
type: "object",
properties: {
slots: {
type: "object",
additionalProperties: {
type: "array",
items: {
type: "object",
properties: {
time: { type: "string", format: "date-time", example: "2024-09-25T08:00:00.000Z" },
},
},
},
},
},
},
},
example: {
status: "success",
data: {
slots: {
"2024-09-25": [{ time: "2024-09-25T08:00:00.000Z" }, { time: "2024-09-25T08:15:00.000Z" }],
"2024-09-26": [
{ time: "2024-09-26T08:00:00.000Z" },
{ time: "2024-09-26T08:15:00.000Z" },
{ time: "2024-09-26T08:30:00.000Z" },
],
},
},
},
},
})
@ApiOperation({ summary: "Get available slots" })
async getAvailableSlots(
@Query() query: GetAvailableSlotsInput,
@@ -22,7 +22,7 @@ import {
BadRequestException,
Headers,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiTags as DocsTags, ApiOperation } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { Request } from "express";
import { stringify } from "querystring";
@@ -40,6 +40,7 @@ export class StripeController {
@Get("/connect")
@UseGuards(ApiAuthGuard)
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Get stripe connect URL" })
async redirect(
@Req() req: Request,
@Headers("Authorization") authorization: string,
@@ -72,6 +73,7 @@ export class StripeController {
@Get("/save")
@UseGuards()
@Redirect(undefined, 301)
@ApiOperation({ summary: "Save stripe credentials" })
async save(
@Query("state") state: string,
@Query("code") code: string,
@@ -95,6 +97,7 @@ export class StripeController {
@Get("/check")
@UseGuards(ApiAuthGuard)
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Check stripe connection" })
async check(@GetUser() user: UserWithProfile): Promise<StripCredentialsCheckOutputResponseDto> {
return await this.stripeService.checkIfStripeAccountConnected(user.id);
}
@@ -2,7 +2,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { Injectable } from "@nestjs/common";
import { Injectable, NotFoundException } from "@nestjs/common";
import type { Profile, User, Team } from "@prisma/client";
export type UserWithProfile = User & {
@@ -223,4 +223,29 @@ export class UsersRepository {
});
return profiles.map((profile) => profile.user);
}
async setDefaultConferencingApp(userId: number, appSlug?: string, appLink?: string) {
const user = await this.findById(userId);
if (!user) {
throw new NotFoundException("user not found");
}
return await this.dbWrite.prisma.user.update({
data: {
metadata:
typeof user.metadata === "object"
? {
...user.metadata,
defaultConferencingApp: {
appSlug: appSlug,
appLink: appLink,
},
}
: {},
},
where: { id: userId },
});
}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -24,7 +24,7 @@
"@storybook/blocks": "^7.6.3",
"@storybook/nextjs": "^7.6.3",
"@storybook/preview-api": "^7.6.3",
"next": "^13.5.4",
"next": "^13.5.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"storybook-addon-rtl-direction": "^0.0.19"
+1 -1
View File
@@ -14,7 +14,7 @@
"dependencies": {
"highlight.js": "^11.6.0",
"isarray": "2.0.5",
"next": "^13.5.4",
"next": "^13.5.6",
"openapi-snippet": "^0.13.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -27,12 +27,12 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyContext = buildLegacyCtx(headers(), cookies(), params, searchParams);
const data = await getData(legacyContext);
const form = "form" in data ? (data.form as { name?: string; description?: string }) : null;
const formName = form?.name ?? "Routing Forms";
const formDescription = form?.description ?? "";
const formName = form?.name;
const formDescription = form?.description;
return await _generateMetadata(
() => formName,
() => formDescription
(t) => formName ?? t("routing_forms"),
(t) => formDescription ?? t("routing_forms_description")
);
};
@@ -11,8 +11,8 @@ import CategoryPage, { type PageProps } from "~/apps/categories/[category]/categ
export const generateMetadata = async () => {
return await _generateMetadata(
() => "Apps Store",
() => "Connecting people, technology and the workplace."
(t) => t("app_store"),
(t) => t("app_store_description")
);
};
+2 -2
View File
@@ -8,8 +8,8 @@ import Page from "~/apps/categories/categories-view";
export const generateMetadata = async () => {
return await _generateMetadata(
() => "Apps Store",
() => "Connecting people, technology and the workplace."
(t) => t("app_store"),
(t) => t("app_store_description")
);
};
@@ -12,8 +12,8 @@ const getData = withAppDirSsg<Y>(getStaticProps);
export const generateMetadata = async () =>
await _generateMetadata(
() => "Bookings",
() => "Create events to share for people to book on your calendar."
(t) => t("bookings"),
(t) => t("bookings_description")
);
export const generateStaticParams = async () => {
+1 -1
View File
@@ -9,7 +9,7 @@ import InsightsPage from "~/insights/insights-view";
export const generateMetadata = async () =>
await _generateMetadata(
() => "Insights",
(t) => t("insights"),
(t) => t("insights_subtitle")
);
@@ -5,8 +5,8 @@ import PlatformView from "~/settings/platform/platform-view";
export const generateMetadata = async () => {
return await _generateMetadata(
() => "Platform",
() => "Manage everything related to platform."
(t) => t("platform"),
(t) => t("platform_description")
);
};
+1
View File
@@ -67,6 +67,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
lang={locale}
dir={direction}
style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}
suppressHydrationWarning
data-nextjs-router="app">
<head nonce={nonce}>
{!!process.env.NEXT_PUBLIC_HEAD_SCRIPTS && (
@@ -1,5 +1,6 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { revalidatePath } from "next/cache";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
@@ -16,9 +17,14 @@ const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
const revalidatePage = async () => {
"use server";
revalidatePath("settings/my-account/general");
};
return (
<SettingsHeader title={t("general")} description={t("general_description")} borderInShellHeader={true}>
<GeneralQueryView />
<GeneralQueryView revalidatePage={revalidatePage} />
</SettingsHeader>
);
};
+1 -1
View File
@@ -42,7 +42,7 @@ export default function EnterprisePage() {
];
return (
<div>
<ShellMain heading="Enterprise" subtitle={t("enterprise_description")}>
<ShellMain heading={t("enterprise")} subtitle={t("enterprise_description")}>
<UpgradeTip
plan="enterprise"
title={t("create_your_org")}
+2
View File
@@ -1,3 +1,5 @@
"use client";
import { DefaultSeo } from "next-seo";
import { Inter } from "next/font/google";
import localFont from "next/font/local";
@@ -598,9 +598,7 @@ function BookingListItem(booking: BookingItemProps) {
<td className="flex w-full flex-col flex-wrap items-end justify-end space-x-2 space-y-2 py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:flex-row sm:flex-nowrap sm:items-start sm:space-y-0 sm:pl-0">
{isUpcoming && !isCancelled ? (
<>
{isPending && (userId === booking.user?.id || booking.isUserTeamAdminOrOwner) && (
<TableActions actions={pendingActions} />
)}
{isPending && <TableActions actions={pendingActions} />}
{isConfirmed && <TableActions actions={bookedActions} />}
{isRejected && <div className="text-subtle text-sm">{t("rejected")}</div>}
</>
@@ -232,7 +232,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
<div className="mt-3 text-center sm:mt-0 sm:text-left" />
<p className="text-emphasis mb-2 ml-1 mt-6 text-sm font-bold">{t("current_location")}:</p>
<p className="text-emphasis mb-2 ml-1 text-sm">
<p className="text-emphasis mb-2 ml-1 break-all text-sm">
{getHumanReadableLocationValue(booking.location, t)}
</p>
<Form
@@ -3,6 +3,7 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import {
Dialog,
@@ -37,7 +38,7 @@ const SecondaryEmailModal = ({
const formMethods = useForm<FormValues>({
resolver: zodResolver(
z.object({
email: z.string().email(),
email: emailSchema,
})
),
});
@@ -24,7 +24,9 @@ export const OAuthClientsDropdown = ({
{Array.isArray(oauthClients) && oauthClients.length > 0 ? (
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button color="secondary">{initialClientName}</Button>
<Button color="secondary" EndIcon="chevron-down">
{initialClientName}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{oauthClients.map((client) => {
+2 -1
View File
@@ -7,6 +7,7 @@ import { z } from "zod";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { emailRegex } from "@calcom/lib/emailSchema";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
@@ -39,7 +40,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
username: z
.string()
.refine((val) => val.trim().length >= 1, { message: t("at_least_characters", { count: 1 }) }),
email_address: z.string().email({ message: t("enter_valid_email") }),
email_address: z.string().regex(emailRegex, { message: t("enter_valid_email") }),
full_name: z.string().min(3, t("at_least_characters", { count: 3 })),
password: z.string().superRefine((data, ctx) => {
const isStrict = true;
+36
View File
@@ -0,0 +1,36 @@
import { CronJob } from "cron";
async function fetchCron(endpoint: string) {
const apiKey = process.env.CRON_API_KEY;
const res = await fetch(`http://localhost:3000/api${endpoint}?${apiKey}`, {
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${process.env.CRON_SECRET}`,
},
});
const json = await res.json();
console.log(endpoint, json);
}
try {
console.log("⏳ Running cron endpoints");
new CronJob(
// Each 5 seconds
"*/5 * * * * *",
async function () {
await Promise.allSettled([
fetchCron("/tasks/cron"),
// fetchCron("/cron/calVideoNoShowWebhookTriggers"),
//
// fetchCron("/tasks/cleanup"),
]);
},
null,
true,
"America/Los_Angeles"
);
} catch (_err) {
console.error("❌ ❌ ❌ Something went wrong ❌ ❌ ❌");
process.exit(1);
}
+5 -11
View File
@@ -1,15 +1,9 @@
import * as Sentry from "@sentry/nextjs";
export function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
});
export async function register() {
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry.server.config");
}
if (process.env.NEXT_RUNTIME === "edge") {
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
});
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "edge") {
await import("./sentry.edge.config");
}
}
+7 -5
View File
@@ -71,10 +71,12 @@ const CustomI18nextProvider = (props: { children: React.ReactElement; i18n?: SSR
**/
const session = useSession();
const locale =
session?.data?.user.locale ?? typeof window !== "undefined"
? window.document.documentElement.lang || "en"
const fallbackLocale =
typeof window !== "undefined" && window.document.documentElement.lang
? window.document.documentElement.lang
: "en";
const newLocale = typeof window !== "undefined" && window.calNewLocale ? window.calNewLocale : null;
const locale = session?.data?.user.locale ?? newLocale ?? fallbackLocale;
useEffect(() => {
try {
@@ -139,10 +141,10 @@ const CalcomThemeProvider = (props: CalcomThemeProps) => {
const embedNamespace = searchParams ? getEmbedNamespace(searchParams) : null;
const isEmbedMode = typeof embedNamespace === "string";
const themeProviderProps = getThemeProviderProps({ props, isEmbedMode, embedNamespace });
const { key, ...themeProviderProps } = getThemeProviderProps({ props, isEmbedMode, embedNamespace });
return (
<ThemeProvider {...themeProviderProps}>
<ThemeProvider key={key} {...themeProviderProps}>
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
{typeof window !== "undefined" && !isEmbedMode && (
+2 -2
View File
@@ -145,10 +145,10 @@ const CalcomThemeProvider = (props: CalcomThemeProps) => {
const embedNamespace = getEmbedNamespace(props.router.query);
const isEmbedMode = typeof embedNamespace === "string";
const themeProviderProps = getThemeProviderProps({ props, isEmbedMode, embedNamespace });
const { key, ...themeProviderProps } = getThemeProviderProps({ props, isEmbedMode, embedNamespace });
return (
<ThemeProvider {...themeProviderProps}>
<ThemeProvider key={key} {...themeProviderProps}>
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
{typeof window !== "undefined" && !isEmbedMode && (
@@ -127,6 +127,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
queryDuration,
eventData.length
),
durationConfig: eventData.metadata?.multipleDuration ?? [],
booking,
user: name,
slug,
+3 -2
View File
@@ -6,20 +6,21 @@ import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiu
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import { emailSchema } from "@calcom/lib/emailSchema";
import slugify from "@calcom/lib/slugify";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
import { ssrInit } from "@server/lib/ssr";
const checkValidEmail = (email: string) => z.string().email().safeParse(email).success;
const checkValidEmail = (email: string) => emailSchema.safeParse(email).success;
const querySchema = z.object({
username: z
.string()
.optional()
.transform((val) => val || ""),
email: z.string().email().optional(),
email: emailSchema.optional(),
});
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
@@ -14,6 +14,8 @@ import type { RouterOutputs } from "@calcom/trpc";
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
import { ssrInit } from "@server/lib/ssr";
const paramsSchema = z.object({
type: z.string().transform((s) => slugify(s)),
slug: z.string().transform((s) => slugify(s)),
@@ -23,17 +25,12 @@ const paramsSchema = z.object({
// 1. Check if team exists, to show 404
// 2. If rescheduling, get the booking details
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getServerSession(context);
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
const {
rescheduleUid,
duration: queryDuration,
isInstantMeeting: queryIsInstantMeeting,
email,
} = context.query;
const { ssrInit } = await import("@server/lib/ssr");
const { req, params, query } = context;
const session = await getServerSession({ req });
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(params);
const { rescheduleUid, isInstantMeeting: queryIsInstantMeeting, email } = query;
const ssr = await ssrInit(context);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req, params?.orgSlug);
const isOrgContext = currentOrgDomain && isValidOrgDomain;
if (!isOrgContext) {
+19 -5
View File
@@ -3,7 +3,8 @@ import type { GetServerSidePropsContext } from "next";
import {
generateGuestMeetingTokenFromOwnerMeetingToken,
setEnableRecordingUIForOrganizer,
setEnableRecordingUIAndUserIdForOrganizer,
updateMeetingTokenIfExpired,
} from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getCalVideoReference } from "@calcom/features/get-cal-video-reference";
@@ -78,21 +79,34 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const oldVideoReference = getCalVideoReference(bookingObj.references);
const endTime = new Date(booking.endTime);
const fourteenDaysAfter = new Date(endTime.getTime() + 14 * 24 * 60 * 60 * 1000);
const epochTimeFourteenDaysAfter = Math.floor(fourteenDaysAfter.getTime() / 1000);
const videoReferencePassword = await updateMeetingTokenIfExpired({
bookingReferenceId: oldVideoReference.id,
roomName: oldVideoReference.uid,
meetingToken: oldVideoReference.meetingPassword,
exp: epochTimeFourteenDaysAfter,
});
// set meetingPassword for guests
if (session?.user.id !== bookingObj.user?.id) {
const guestMeetingPassword = await generateGuestMeetingTokenFromOwnerMeetingToken(
oldVideoReference.meetingPassword
videoReferencePassword,
session?.user.id
);
bookingObj.references.forEach((bookRef) => {
bookRef.meetingPassword = guestMeetingPassword;
});
}
// Only for backward compatibility for organizer
// Only for backward compatibility and setting user id in particpants for organizer
else {
const meetingPassword = await setEnableRecordingUIForOrganizer(
const meetingPassword = await setEnableRecordingUIAndUserIdForOrganizer(
oldVideoReference.id,
oldVideoReference.meetingPassword
videoReferencePassword,
session?.user.id
);
if (!!meetingPassword) {
bookingObj.references.forEach((bookRef) => {
+4 -2
View File
@@ -98,10 +98,12 @@ export default function Apps({ categories, appStore, userAdminTeams }: PageProps
}
export const LayoutWrapper = (page: React.ReactElement) => {
const { t } = useLocale();
return (
<Shell
title="Apps Store"
description="Create forms to direct attendees to the correct destinations."
title={t("app_store")}
description={t("app_store_description")}
withoutMain={true}
hideHeadingOnMobile>
{page}
@@ -21,8 +21,8 @@ export default function Apps({ apps }: PageProps) {
<Shell
isPublic
backPath="/apps"
title="Apps Store"
description="Connecting people, technology and the workplace."
title={t("app_store")}
description={t("app_store_description")}
smallHeading
heading={
<>
@@ -15,12 +15,7 @@ export default function Apps({ categories }: PageProps) {
const { t, isLocaleReady } = useLocale();
return (
<Shell
isPublic
large
hideHeadingOnMobile
title="Apps Store"
description="Connecting people, technology and the workplace.">
<Shell isPublic large hideHeadingOnMobile title={t("app_store")} description={t("app_store_description")}>
<div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0">
<Link
href="/apps"
+2 -1
View File
@@ -12,6 +12,7 @@ import { z } from "zod";
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { HOSTED_CAL_FEATURES, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { emailRegex } from "@calcom/lib/emailSchema";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLastUsed, LastUsed } from "@calcom/lib/hooks/useLastUsed";
@@ -59,7 +60,7 @@ PageProps & WithNonceProps<{}>) {
email: z
.string()
.min(1, `${t("error_required_field")}`)
.email(`${t("enter_valid_email")}`),
.regex(emailRegex, `${t("enter_valid_email")}`),
...(!!totpEmail ? {} : { password: z.string().min(1, `${t("error_required_field")}`) }),
})
// Passthrough other fields like totpCode
@@ -213,11 +213,11 @@ export default function AvailabilityPage({ currentOrg }: PageProps) {
<div>
<Shell
heading={t("availability")}
title="Availability"
description="Configure times when you are available for bookings."
subtitle={t("configure_availability")}
title={t("availability")}
description={t("configure_availability")}
hideHeadingOnMobile
withoutMain={false}
subtitle={t("configure_availability")}
CTA={
<div className="flex gap-2">
<ToggleGroup
@@ -151,8 +151,8 @@ export default function Bookings() {
hideHeadingOnMobile
heading={t("bookings")}
subtitle={t("bookings_description")}
title="Bookings"
description="Create events to share for people to book on your calendar.">
title={t("bookings")}
description={t("bookings_description")}>
<div className="flex flex-col">
<div className="flex flex-row flex-wrap justify-between">
<HorizontalTabs tabs={tabs} />
@@ -161,7 +161,11 @@ export default function Success(props: PageProps) {
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
const [comment, setComment] = useState("");
const parsedRating = rating ? parseInt(rating, 10) : 3;
const currentUserEmail = searchParams?.get("cancelledBy") ?? session?.user?.email ?? undefined;
const currentUserEmail =
searchParams?.get("rescheduledBy") ??
searchParams?.get("cancelledBy") ??
session?.user?.email ??
undefined;
const defaultRating = isNaN(parsedRating) ? 3 : parsedRating > 5 ? 5 : parsedRating < 1 ? 1 : parsedRating;
const [rateValue, setRateValue] = useState<number>(defaultRating);
@@ -16,6 +16,7 @@ export default function Type({
entity,
duration,
hashedLink,
durationConfig,
}: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
@@ -35,6 +36,7 @@ export default function Type({
entity={entity}
duration={duration}
hashedLink={hashedLink}
durationConfig={durationConfig}
/>
</main>
);
@@ -19,7 +19,6 @@ import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsF
import Shell from "@calcom/features/shell/Shell";
import { parseEventTypeColor } from "@calcom/lib";
import { APP_NAME } from "@calcom/lib/constants";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
@@ -250,6 +249,7 @@ export const InfiniteEventTypeList = ({
const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = useState<SchedulingType | null>(
null
);
const [privateLinkCopyIndices, setPrivateLinkCopyIndices] = useState<Record<string, number>>({});
const utils = trpc.useUtils();
const mutation = trpc.viewer.eventTypeOrder.useMutation({
@@ -443,8 +443,11 @@ export const InfiniteEventTypeList = ({
return page?.eventTypes?.map((type, index) => {
const embedLink = `${group.profile.slug}/${type.slug}`;
const calLink = `${bookerUrl}/${embedLink}`;
const isPrivateURLEnabled = type.hashedLink?.link;
const placeholderHashedLink = `${WEBSITE_URL}/d/${type.hashedLink?.link}/${type.slug}`;
const isPrivateURLEnabled =
type.hashedLink && type.hashedLink.length > 0
? type.hashedLink[privateLinkCopyIndices[type.slug] ?? 0]?.link
: "";
const placeholderHashedLink = `${bookerUrl}/d/${isPrivateURLEnabled}/${type.slug}`;
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
const isChildrenManagedEventType =
type.metadata?.managedEventConfig !== undefined &&
@@ -544,6 +547,11 @@ export const InfiniteEventTypeList = ({
onClick={() => {
showToast(t("private_link_copied"), "success");
navigator.clipboard.writeText(placeholderHashedLink);
setPrivateLinkCopyIndices((prev) => {
const prevIndex = prev[type.slug] ?? 0;
prev[type.slug] = (prevIndex + 1) % type.hashedLink.length;
return prev;
});
}}
/>
</Tooltip>
+3 -3
View File
@@ -53,10 +53,10 @@ export default function InsightsPage() {
<div>
<Shell
withoutMain={false}
heading="Insights"
heading={t("insights")}
subtitle={t("insights_subtitle")}
title="Insights"
description="View booking insights across your events.">
title={t("insights")}
description={t("insights_subtitle")}>
<UpgradeTip
plan="team"
title={t("make_informed_decisions")}
+2 -1
View File
@@ -1,6 +1,7 @@
"use client";
import Shell, { MobileNavigationMoreItems } from "@calcom/features/shell/Shell";
import Shell from "@calcom/features/shell/Shell";
import { MobileNavigationMoreItems } from "@calcom/features/shell/navigation/Navigation";
import { useLocale } from "@calcom/lib/hooks/useLocale";
export default function MorePage() {
@@ -192,15 +192,16 @@ const AppearanceView = ({
</div>
<Form
form={userAppThemeFormMethods}
handleSubmit={(values) => {
handleSubmit={({ appTheme }) => {
if (appTheme === "system") appTheme = null;
mutation.mutate({
appTheme: values.appTheme === "" ? null : values.appTheme,
appTheme,
});
}}>
<div className="border-subtle flex flex-col justify-between border-x px-6 py-8 sm:flex-row">
<ThemeLabel
variant="system"
value={undefined}
value="system"
label={t("theme_system")}
defaultChecked={user.appTheme === null}
register={userAppThemeFormMethods.register}
@@ -225,6 +226,7 @@ const AppearanceView = ({
</div>
<SectionBottomActions className="mb-6" align="end">
<Button
loading={mutation.isPending}
disabled={isUserAppThemeSubmitting || !isUserAppThemeDirty}
type="submit"
data-testid="update-app-theme-btn"
@@ -244,17 +246,21 @@ const AppearanceView = ({
</div>
<Form
form={userThemeFormMethods}
handleSubmit={(values) => {
handleSubmit={({ theme }) => {
if (theme === "light" || theme === "dark") {
mutation.mutate({
theme,
});
return;
}
mutation.mutate({
// Radio values don't support null as values, therefore we convert an empty string
// back to null here.
theme: values.theme === "" ? null : values.theme,
theme: null,
});
}}>
<div className="border-subtle flex flex-col justify-between border-x px-6 py-8 sm:flex-row">
<ThemeLabel
variant="system"
value={undefined}
value="system"
label={t("theme_system")}
defaultChecked={user.theme === null}
register={userThemeFormMethods.register}
@@ -58,7 +58,6 @@ const ConferencingView = () => {
query={query}
customLoader={<SkeletonLoader />}
success={({ data }) => {
console.log(data);
if (!data.items.length) {
return (
<EmptyScreen
@@ -68,9 +68,14 @@ interface GeneralViewProps {
localeProp: string;
user: RouterOutputs["viewer"]["me"];
travelSchedules: RouterOutputs["viewer"]["getTravelSchedules"];
revalidatePage: GeneralQueryViewProps["revalidatePage"];
}
const GeneralQueryView = () => {
type GeneralQueryViewProps = {
revalidatePage: () => Promise<void>;
};
const GeneralQueryView = ({ revalidatePage }: GeneralQueryViewProps) => {
const { t } = useLocale();
const { data: user, isPending } = trpc.viewer.me.useQuery();
@@ -82,10 +87,17 @@ const GeneralQueryView = () => {
if (!user) {
throw new Error(t("something_went_wrong"));
}
return <GeneralView user={user} travelSchedules={travelSchedules || []} localeProp={user.locale} />;
return (
<GeneralView
user={user}
travelSchedules={travelSchedules || []}
localeProp={user.locale}
revalidatePage={revalidatePage}
/>
);
};
const GeneralView = ({ localeProp, user, travelSchedules }: GeneralViewProps) => {
const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: GeneralViewProps) => {
const utils = trpc.useContext();
const {
t,
@@ -105,6 +117,7 @@ const GeneralView = ({ localeProp, user, travelSchedules }: GeneralViewProps) =>
if (res.locale) {
window.calNewLocale = res.locale;
}
await revalidatePage();
},
onError: () => {
showToast(t("error_updating_settings"), "error");
@@ -177,7 +190,7 @@ const GeneralView = ({ localeProp, user, travelSchedules }: GeneralViewProps) =>
<div>
<Form
form={formMethods}
handleSubmit={(values) => {
handleSubmit={async (values) => {
setIsUpdateBtnLoading(true);
mutation.mutate({
...values,
@@ -12,6 +12,7 @@ import { z } from "zod";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { emailSchema } from "@calcom/lib/emailSchema";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
@@ -523,12 +524,12 @@ const ProfileForm = ({
.max(FULL_NAME_LENGTH_MAX_LIMIT, {
message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }),
}),
email: z.string().email(),
email: emailSchema,
bio: z.string(),
secondaryEmails: z.array(
z.object({
id: z.number(),
email: z.string().email(),
email: emailSchema,
emailVerified: z.union([z.string(), z.null()]).optional(),
emailPrimary: z.boolean().optional(),
})
@@ -7,6 +7,7 @@ import Shell from "@calcom/features/shell/Shell";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
import { PlatformPricing } from "@calcom/web/components/settings/platform/pricing/platform-pricing/index";
import NoPlatformPlan from "@components/settings/platform/dashboard/NoPlatformPlan";
import { useGetUserAttributes } from "@components/settings/platform/hooks/useGetUserAttributes";
@@ -23,13 +24,33 @@ export default function PlatformBillingUpgrade() {
const onContactSupportClick = async () => {
await open();
};
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, userBillingData } = useGetUserAttributes();
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, userBillingData, isPaidUser, userOrgId } =
useGetUserAttributes();
if (isUserLoading || (isUserBillingDataLoading && !userBillingData)) {
return <div className="m-5">Loading...</div>;
}
if (!isPlatformUser) return <NoPlatformPlan />;
if (isPlatformUser && !isPaidUser)
return (
<PlatformPricing
teamId={userOrgId}
heading={
<div className="mb-5 text-center text-2xl font-semibold">
<h1>Subscribe to Platform</h1>
</div>
}
/>
);
if (!isPlatformUser)
return (
<div>
<Shell isPlatformUser={true} hideHeadingOnMobile withoutMain={false} SidebarContainer={<></>}>
<NoPlatformPlan />
</Shell>
</div>
);
return (
<div>
@@ -69,6 +69,14 @@ export default function EditOAuthClientWebhooks() {
value: WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED,
label: "recording_transcription_generated",
},
{
value: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
label: "after_hosts_cal_video_no_show",
},
{
value: WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
label: "after_guests_cal_video_no_show",
},
]}
onSubmit={async (data) => {
try {
@@ -4,6 +4,7 @@ import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { useState, useEffect } from "react";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { showToast } from "@calcom/ui";
import {
@@ -22,6 +23,7 @@ import { PlatformPricing } from "@components/settings/platform/pricing/platform-
const queryClient = new QueryClient();
export default function Platform() {
const { t } = useLocale();
const [initialClientId, setInitialClientId] = useState("");
const [initialClientName, setInitialClientName] = useState("");
@@ -75,11 +77,12 @@ export default function Platform() {
<QueryClientProvider client={queryClient}>
<div>
<Shell
heading="Platform"
title="Platform"
heading={t("platform")}
subtitle={t("platform_description")}
title={t("platform")}
description={t("platform_description")}
hideHeadingOnMobile
withoutMain={false}
subtitle="Manage everything related to platform."
isPlatformUser={true}>
<HelpCards />
<OAuthClientsList oauthClients={data} isDeleting={isDeleting} handleDelete={handleDelete} />
+3 -3
View File
@@ -19,10 +19,10 @@ function Teams(props: PageProps) {
<Shell
withoutMain={false}
heading={t("teams")}
title="Teams"
description="Create and manage teams to use collaborative features."
hideHeadingOnMobile
subtitle={t("create_manage_teams_collaborative")}
title={t("teams")}
description={t("create_manage_teams_collaborative")}
hideHeadingOnMobile
CTA={
(!user.organizationId || user.organization.isOrgAdmin) && (
<Button
+3 -2
View File
@@ -178,6 +178,7 @@ const nextConfig = {
serverComponentsExternalPackages: ["next-i18next"],
optimizePackageImports: ["@calcom/ui"],
instrumentationHook: true,
serverActions: true,
},
i18n: {
...i18n,
@@ -291,8 +292,8 @@ const nextConfig = {
destination: "/apps/routing-forms/routing-link/:formQuery*",
},
{
source: "/router",
destination: "/apps/routing-forms/router",
source: "/router/:path*",
destination: "/apps/routing-forms/router/:path*",
},
{
source: "/success/:path*",
+8 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "4.5.8",
"version": "4.6.0",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -8,6 +8,7 @@
"analyze:browser": "BUNDLE_ANALYZE=browser next build",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
"dev": "next dev",
"dev:cron": "ts-node cron-tester.ts",
"dev-https": "NODE_TLS_REJECT_UNAUTHORIZED=0 next dev --experimental-https",
"dx": "yarn dev",
"test-codegen": "yarn playwright codegen http://localhost:3000",
@@ -79,6 +80,7 @@
"bcp-47-match": "^2.0.3",
"bcryptjs": "^2.4.3",
"classnames": "^2.3.1",
"dompurify": "^3.1.7",
"dotenv-cli": "^6.0.0",
"entities": "^4.4.0",
"eslint-config-next": "^13.2.1",
@@ -97,7 +99,7 @@
"memory-cache": "^0.2.0",
"micro": "^10.0.1",
"mime-types": "^2.1.35",
"next": "^13.5.4",
"next": "^13.5.6",
"next-auth": "^4.22.1",
"next-axiom": "^0.17.0",
"next-collect": "^0.2.1",
@@ -105,6 +107,7 @@
"next-seo": "^6.0.0",
"next-themes": "^0.2.0",
"nodemailer": "^6.7.8",
"nuqs": "^1.20.0",
"otplib": "^12.0.1",
"qrcode": "^1.5.1",
"raw-body": "^2.5.1",
@@ -121,7 +124,6 @@
"react-multi-email": "^0.5.3",
"react-phone-input-2": "^2.15.1",
"react-phone-number-input": "^3.2.7",
"react-query": "^3.39.3",
"react-schemaorg": "^2.0.0",
"react-select": "^5.7.0",
"react-timezone-select": "^1.4.0",
@@ -151,6 +153,7 @@
"@types/async": "^3.2.15",
"@types/bcryptjs": "^2.4.2",
"@types/detect-port": "^1.3.2",
"@types/dompurify": "^3.0.5",
"@types/glidejs__glide": "^3.4.2",
"@types/lodash": "^4.14.182",
"@types/markdown-it": "^12.2.3",
@@ -170,6 +173,8 @@
"@types/uuid": "8.3.1",
"autoprefixer": "^10.4.12",
"copy-webpack-plugin": "^11.0.0",
"cron": "^3.1.7",
"deasync": "^0.1.30",
"detect-port": "^1.3.0",
"env-cmd": "^10.1.0",
"module-alias": "^2.2.2",
+1 -1
View File
@@ -33,7 +33,7 @@ export default function Custom404() {
const links = [
{
title: "Enterprise",
title: t("enterprise"),
description: "Learn more about organizations and subdomains in our enterprise plan.",
icon: "shield" as const,
href: `${WEBSITE_URL}/enterprise`,
+33 -1
View File
@@ -67,7 +67,39 @@ class MyDocument extends Document<Props> {
nonce={nonce}
id="newLocale"
dangerouslySetInnerHTML={{
__html: `window.calNewLocale = "${newLocale}";`,
__html: `
window.calNewLocale = "${newLocale}";
(function applyTheme() {
try {
const appTheme = localStorage.getItem('app-theme');
if (!appTheme) return;
let bookingTheme, username;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('booking-theme:')) {
bookingTheme = localStorage.getItem(key);
username = key.split("booking-theme:")[1];
break;
}
}
const onReady = () => {
const isBookingPage = username && window.location.pathname.slice(1).startsWith(username);
if (document.body) {
document.body.classList.add(isBookingPage ? bookingTheme : appTheme);
} else {
requestAnimationFrame(onReady);
}
};
requestAnimationFrame(onReady);
} catch (e) {
console.error('Error applying theme:', e);
}
})();
`,
}}
/>
<link rel="apple-touch-icon" sizes="180x180" href="/api/logo?type=apple-touch-icon" />
+2 -6
View File
@@ -1,17 +1,13 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { passwordResetRequest } from "@calcom/features/auth/lib/passwordResetRequest";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { emailSchema } from "@calcom/lib/emailSchema";
import { defaultHandler } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const email = z
.string()
.email()
.transform((val) => val.toLowerCase())
.safeParse(req.body?.email);
const email = emailSchema.transform((val) => val.toLowerCase()).safeParse(req.body?.email);
if (!email.success) {
return res.status(400).json({ message: "email is required" });
+2 -1
View File
@@ -3,6 +3,7 @@ import z from "zod";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { emailRegex } from "@calcom/lib/emailSchema";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import slugify from "@calcom/lib/slugify";
@@ -14,7 +15,7 @@ const querySchema = z.object({
.string()
.refine((val) => val.trim().length >= 1, { message: "Please enter at least one character" }),
full_name: z.string().min(3, "Please enter at least 3 characters"),
email_address: z.string().email({ message: "Please enter a valid email" }),
email_address: z.string().regex(emailRegex, { message: "Please enter a valid email" }),
password: z.string().refine((val) => isPasswordValid(val.trim(), false, true), {
message:
"The password must be a minimum of 15 characters long containing at least one number and have a mixture of uppercase and lowercase letters",
+7 -5
View File
@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments";
import { TeamBilling } from "@calcom/ee/billing/teams";
import prisma from "@calcom/prisma";
const querySchema = z.object({
@@ -32,6 +32,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
select: {
id: true,
metadata: true,
isOrganization: true,
parentId: true,
},
skip: pageNumber * pageSize,
take: pageSize,
@@ -41,10 +44,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
break;
}
for (const team of teams) {
await updateQuantitySubscriptionFromStripe(team.id);
await delay(100); // Adjust the delay as needed to avoid rate limiting
}
const teamsBilling = TeamBilling.initMany(teams);
const teamBillingPromises = teamsBilling.map((teamBilling) => teamBilling.updateQuantity());
await Promise.allSettled(teamBillingPromises);
pageNumber++;
}
+7
View File
@@ -0,0 +1,7 @@
export const config = {
api: {
bodyParser: false,
},
};
export { default } from "@calcom/features/ee/billing/api/webhook";
+2 -1
View File
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import getRawBody from "raw-body";
import z from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { default as webPrisma } from "@calcom/prisma";
export const config = {
@@ -13,7 +14,7 @@ export const config = {
const helpscoutRequestBodySchema = z.object({
customer: z.object({
email: z.string().email(),
email: emailSchema,
}),
});
+4
View File
@@ -3,6 +3,8 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Meta } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
import type { PageProps } from "~/auth/setup-view";
@@ -17,6 +19,8 @@ const Page = (props: PageProps) => {
</>
);
};
Page.PageWrapper = PageWrapper;
export default Page;
export { getServerSideProps };

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