Compare commits

...
136 Commits
Author SHA1 Message Date
GitHub Actions a08ae43533 chore: release v4.7.17
Post release / release (push) Canceled after 0s
2024-12-04 18:14:31 +00:00
Benny Joo d60b6c6a6d chore: settings/teams and settings/billing - remove pages router and use app router (#16751)
* remove pages router and use app router

* revert yarn.lock

* remove line break from yarn lock

* Add booking limits page to app dir

* revert yarn.lock

* do not render bookingLimits from pages router

* clean up code

* remove isAppDir

* revert unneeded change

* add layout to billing page

* remove files not needed

* rename imports

* remove APP_ROUTER_SETTINGS_DEVELOPER

* remove layout
2024-12-04 12:31:45 -05:00
Keith Williams 9085287fc6 revert: "chore: Move pre-commit commands to pre-push (#17996)" (#18009)
This reverts commit f63c06d68a.
2024-12-04 17:17:58 +00:00
Benny Joo 7e2b5b12b4 chore: settings/developer - remove pages router and use app router (#16794)
* remove pages router and use app router

* remove Meta / improve code quality

* remove isAppDir

* refactor code for better code

* fix test

* remove meta

* improve tests
2024-12-04 11:00:37 -05:00
sean-brydon 3211c5bcff fix data loading state (#18002) 2024-12-04 15:15:01 +00:00
Christoffer Bjelke a75a5b72b2 chore: enable eslint rule react/no-danger (#17983)
* enable react/no-danger and disable rule all places

* remove wild rule
2024-12-04 15:07:30 +00:00
Rajdeep Ghosh 509fc4b289 style: rename [...nextauth].tsx to use .ts file extention (#17992) 2024-12-04 14:52:31 +00:00
Keith Williams f63c06d68a chore: Move pre-commit commands to pre-push (#17996)
* chore: Move pre-commit commands to pre-push

* Is this fast

* Removed test commit

* Adding logging
2024-12-04 14:51:26 +00:00
Anik Dhabal Babu 3dff77fe59 chore: don't show parent managed eventType as event redirectionoption (#17993) 2024-12-04 20:08:52 +05:30
Calcom BotandReplexica a6f9952d75 feat: update translations via @replexica (#18000)
Co-authored-by: Replexica <support@replexica.com>
2024-12-04 13:13:51 +00:00
Benny JooandKeith Williams 03704b2e73 feat: fetch SEO data for booking pages in server (#17987)
* feat: fetch public event data for booking pages server-side

* fix types

* refactor

* Replace the getPublicEvent with other smaller calls

* Change username to teamSlug

* Fixing types

* Type checks

* Pushing latest for Benny - this still has type errors

* fix type checks

* fill data for team pages too

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-12-04 14:08:27 +01:00
sean-brydon 982a715c4b feat: add assignment reason to routing insights (#17984)
* add assignment reason

* add routing reason to csv export
2024-12-04 12:39:20 +00:00
Morgan eaff9e8c89 fix: disable mandatory reminder for platform (#17998)
* fix: disable mandatory reminder for platform

* bump libraries version

* fixup! bump libraries version
2024-12-04 12:33:53 +00:00
e2938ac39a refactor: v2 OAuth webhooks & workflows (#17959)
* refactor: handleMarkNoShow OAuth webhook handling

* refactor: confirm / decline booking OAuth webhook handling

* refactor: pass oauth client to calendar event

* refactor: pass oauth params

* fix import

* fix tests

* chore: bump libraries

* chore: bump libraries

* chore: republish platform libraries

* chore: republish platform libraries

* fix: use replexica key in v2 and unit test CI

* lock file

* fixup! Merge branch 'main' into lauris/cal-4807-platform-refactor-oauth-webhooks

* Revert "fix: use replexica key in v2 and unit test CI"

This reverts commit 0bd6c364535f9e11a4d1497a6bd8909885562080.

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-12-04 11:25:20 +01:00
Morgan b92db55380 chore: add REPLEXICA env to ci tests (#17970)
* chore: add REPLEXICA env to ci tests

* fixup! chore: add REPLEXICA env to ci tests
2024-12-04 09:37:25 +00:00
Somay Chauhan 3015c5a15e fix: trying to update the timeFormat throws error (#17991)
* timeFormat should be an intiger
2024-12-04 09:29:51 +01:00
Joe Au-YeungandUdit Takkar 2444598e38 Fuzzy search account website, fallback to contacts (#17989)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-04 07:46:51 +00:00
Benny Joo 6eadadec73 fix: remove duplicate header in attributes page (#17985) 2024-12-03 20:32:56 +00:00
sean-brydon 145449e7b4 fix:z index hover card on routing form responses (#17981)
* Revert "merge columns (#17938)"

This reverts commit a3ceefc00e.

* fix hovercard on responses
2024-12-03 19:00:56 +00:00
1d8326ed83 feat: watchlist (#17947)
* lock users on signup if their email is in blacklist

* add to turbo env list

* prevent locked user or blocked email domain from using API

* Refactored to only run 1 query to find the user

* Fixing tests

* WIP

* WIP

* WIP

* Discard changes to turbo.json

* Fixed tests

* Update isLockedOrBlocked.test.ts

* Update isAdmin.integration-test.ts

* Update tsconfig.json

* chore: rename to watchlist

Signed-off-by: Omar López <zomars@me.com>

---------

Signed-off-by: Omar López <zomars@me.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-12-03 18:43:01 +01:00
GitHub Actions 59993891b8 chore: release v4.7.16
Post release / release (push) Canceled after 0s
2024-12-03 17:36:59 +00:00
Alex van Andel a31d140761 fix: handling of defaultLabel (#17976) 2024-12-03 17:28:02 +00:00
Calcom BotandReplexica b055fe182a feat: update translations via @replexica (#17965)
Co-authored-by: Replexica <support@replexica.com>
2024-12-03 17:19:32 +00:00
392757fb55 fix: Remove user from managed event type when removing from org (#17369)
* Remove user from managed event type when removing from org

* added test for admin check, correct changes for membership, user info, profiles

* remove host assignment for removed members

* expand test to cover  event types removal

* chore: use var

* chore

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-12-03 17:54:04 +01:00
3222be9171 docs: v2 rolling window and authentication (#17825)
* docs: booking window rolling description

* docs: v2 auth and navigating endpoints

* Update apps/api/v2/swagger/documentation.json

* Update docs/api-reference/v2/introduction.mdx

* Update docs/api-reference/v2/introduction.mdx

* Update docs/api-reference/v2/introduction.mdx

* docs: refresh and force-refresh endpoints

* docs: booking fields and toasts guides

* fix docs

* fix: bookings docs

* docs: booking-redirects usebooking

* Update booking-fields.mdx

* Update booking-fields.mdx

* Update booking-fields.mdx

* Update booking-window.input.ts

* Update replacing-toasts.mdx

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-12-03 17:53:14 +01:00
Benny Joo 6378729962 chore: use markdownToSafeHTML in EventMeta (#17975) 2024-12-03 16:45:48 +00:00
Udit Takkar 1c5786b66f fix: org bio (#17971)
* fix: org bio

* chore: use in bookings
2024-12-03 16:30:03 +00:00
Peer Richelsen e61a0d8081 chore: safeMarkdown (#17972) 2024-12-03 15:47:58 +00:00
Rajiv SahalandMorgan Vernay 0cb955cc0b fix: build failure for v4.7.15 (#17968)
* fix: type error causing broken build

* fix typings

* fixup! fix typings

* fixup! fixup! fix typings

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-12-03 15:44:42 +00:00
Hariom BalharaandAlex van Andel 00689fda0a fix: remove platform and console submodule (#17939)
* Fix: remove platform submodule

* Update git-setup.sh

* Update git-setup.sh

* Update git-setup.sh

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-03 11:49:52 +00:00
Joe Au-Yeung bd83b36b75 feat: Salesforce - add option to ignore guests (#17956)
* Enable option to ignore guests

* Ignore guests in CRM Manager
2024-12-03 11:19:57 +00:00
Eunjae Lee 05e2029bc9 fix: change case for column size vars (#17961)
* fix: change case for column size vars

* fix wrong import
2024-12-03 09:50:00 +00:00
Udit Takkar 9e3d369ac3 feat: return participants (#17950)
* feat: return participants

* fix: update tests
2024-12-03 09:36:59 +00:00
sean-brydon 356c603e3f Revert "merge columns (#17938)" (#17960)
This reverts commit a3ceefc00e.
2024-12-03 09:23:28 +00:00
5afe3aa54d feat: update translations via @replexica (#17952)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-03 06:56:08 +00:00
Alex van Andel 985f216a43 test: fix organisation settings e2e (#17957) 2024-12-03 09:41:20 +05:30
Anwar SadathandHariom 3a472997ff fix: brand colors set by user not respected in embed (#17399)
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-12-03 04:10:58 +00:00
GitHub Actions 6dcb6aece4 chore: release v4.7.15
Post release / release (push) Canceled after 0s
2024-12-02 22:51:37 +00:00
GitHub Actions dc7ee1aba2 chore: release vv4.7.15 2024-12-02 22:41:39 +00:00
Joe Au-Yeung 2f9f29511b Reduce banner height on mobile booker (#17954) 2024-12-02 22:30:16 +00:00
Anik Dhabal BabuandPeer Richelsen 0aa58c1095 fix: Date Range Picker behavior (#17914)
* fix: Date Range Picker behaviour

* remove console log

* update

* update

* update css

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-12-03 02:10:23 +05:30
Eunjae LeeandAlex van Andel 58b77b2a16 fix: fix date comparison for OOO (#17756)
* fix: fix date comparison for OOO

* fix date comparison with today, and add tests

* remove wrong code

* remove logic that is no longer in use

* fix type error

* mock more methods

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-02 19:39:44 +00:00
Peer Richelsen caeaf9053b chore: fixed colored event margin (#17932) 2024-12-02 19:31:07 +00:00
fe209df2be fix: ai translations description for atoms (#17875)
* remove ai translations description for atoms

* PR feedback

* fixup

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-12-02 19:27:54 +00:00
CodeWithAdnanandPrashant Varma 2e7a5a84a6 feat: Show add to calendar section for customers after booking an event (#17761)
* Show 'Add to calendar' section for customers after booking an event

* removed comment

---------

Co-authored-by: Prashant Varma <prashantvarma5083@gmail.com>
2024-12-02 19:25:56 +00:00
Anik Dhabal Babu 410558f822 fix: Display option label in the routing form response email (#17898)
* fix: display option label int the routing form response email

* Update

* fix type error

* type error
2024-12-02 19:24:11 +00:00
sean-brydon a3ceefc00e merge columns (#17938) 2024-12-02 19:21:42 +00:00
Kartik SainiandAlex van Andel c524a3c1ef fix: host entry not created when user added to team in org (#17927)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-02 19:19:05 +00:00
a5ba2682f6 fix: /members improvements (#17911)
* remove heading for more space

* fix: mobile support for mass attribute assigning bar

* change the order of Members item on Navigation

* refactor NavigationItem to support moreOnMobile property

* fix logic for nav

* decrease column widths on member list

* improve mobile design for mass attribute assign

* adjust mobile ui for DataTableSelectionBar

* add key to EventTypesList

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-12-02 19:13:08 +00:00
Anik Dhabal Babu 51c91e41da fix: navigation items weird behavior (#17929) 2024-12-02 19:08:13 +00:00
Alex van Andel cf6654dd1f Extract some refactor work from util.ts to improve error handling (#17912) 2024-12-02 19:06:45 +00:00
Benny Joo d057d7d37f fix: refactor OG link builder (#17897)
* fix: manually encode special characters

* fix test

* fix

* fix
2024-12-02 19:01:09 +00:00
Anik Dhabal BabuandAlex van Andel 262738f288 fix: Meeting URL does not save when changing the location to Gmeet or any static app (#17840)
* fix: meetingurl does not save when we change location

* update

* fix type error

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-02 19:00:43 +00:00
Eunjae Lee 27cd2457e8 fix: fix UI regression related to DataTable (#17943)
* fix: fix UI regression regarding DataTable

* update comment
2024-12-02 18:54:58 +00:00
Vishnu b33ca2f937 feat: no show guests on insights (#17919)
* feat: return noShowGuest data from eventsByStatus & eventsTimeline procedures

* feat: add noShowGuest Data in BookingKPICards

* feat: add noShowGuest Data in BookingStatusLineChart

* feat: track noShowGuests in outer query

* chore: fix noShowGuest naming
2024-12-02 18:53:32 +00:00
Alex van Andel 82f64f7d6a fix: Broken integration test due to month change (#17949) 2024-12-02 18:26:45 +00:00
Amit Sharma 8052d27d0b feat: show attributes in profile (#17920) 2024-11-30 22:53:05 +05:30
Aditya Pandey 5720ec8038 fix : Issue on docs (#17895) 2024-11-29 19:50:57 +00:00
Vincent Lam 01b1f5bcc8 fix: Added proper updates/interactivity on the "Getting started" app integ… (#17903)
* Added proper updates/interactivity on the "Getting started" app integrations.

- InstallAppButtonWithoutPlanCheck properly vends the mutation data in the render's props
- Fixed the improper props wiring in the AppConnectionItem
- Added a loading state for reactivity

* -Added toaster for error/success confirmation
2024-11-29 19:14:01 +00:00
Jesse KlotzandHariom 953a8119db build: Reduce bundle size from unnecessary bundeling react/jsx-runtime in embed-react package (#17790)
* Update vite.config.js

* linting fix

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-11-29 22:19:16 +05:30
Eunjae Lee e8daa4f92f fix: bulk attribute assignment (#17896)
remove comment
2024-11-29 10:00:20 +00:00
f631abfdc4 fix: #16028 Org Settings - SEOIndexing,Disable Org Url (#16075)
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2024-11-29 10:21:14 +05:30
GitHub Actions b5c6d7af91 chore: release v4.7.14
Post release / release (push) Canceled after 0s
2024-11-28 21:50:59 +00:00
Omar López 249bcaca68 fix: locked users admin list and bypass query (#17902) 2024-11-28 21:45:40 +00:00
Joe Au-Yeung f2efd6dc28 feat: Salesforce - send routing form responses to Salesforce (#17890) 2024-11-28 12:37:45 -07:00
Joe Au-Yeung be2f0f986e fix: getAttributeRoutingConfig find the route based on the chosenRouteId (#17867)
## What does this PR do?

<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->

We were parsing routes based on `eventTypeId` but routes could be pointing to the same event type. This was causing misroutings.

- Fixes #XXXX (GitHub issue number)
- Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description)

<!-- Please provide a loom video for visual changes to speed up reviews
 Loom Video: https://www.loom.com/
-->

## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [ ] 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.
2024-11-28 12:31:06 -07:00
Udit TakkarandAlex van Andel 589c1dccb9 fix: remove from host when removed from org (#17893)
* fix: remove from host when removed from org

* test: add test for removing members

* fix: type errors, some unused params

* fix: test

* refactor: use prisma.count

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-11-28 15:56:17 +00:00
Eunjae Lee 4eac6c755d fix: revert bg color for pinned column at data table (#17892) 2024-11-28 10:04:53 +01:00
Lauris Skraucis 131b3d0984 refactor: v2 validate booking fields (#17878)
* refactor: v2 validate booking fields

* test
2024-11-28 10:37:42 +02:00
Omar López 693356d3c4 fix: Exclude locked users by default (#17889) 2024-11-27 17:03:58 -07:00
GitHub Actions 4b717108b0 chore: release v4.7.13
Post release / release (push) Canceled after 0s
2024-11-27 22:46:23 +00:00
Peer Richelsen ad8c888b77 chore: temporarily remove push notifications button until we have proper support (#17887) 2024-11-27 19:27:07 +00:00
Benny Joo d6fe955ea8 feat: hide the update button in case of no permission (#17883)
* feat: hide button if not owner of org

* fix
2024-11-27 11:43:46 -05:00
Benny Joo ab9b6024bb feat: add tooltip to button for better UX (#17881) 2024-11-27 15:47:08 +00:00
Benny Joo a7e99f3674 fix: Remove double encoding for OG image URLs (#17872)
* fix: do not use vercel image api for meeting link preview image

* fix

* fix

* remove double encoding

* fix

* fix test

* fix

* fix

* fix test

* fix

* fix

* remove test tag

* fix
2024-11-27 15:45:54 +00:00
Lauris Skraucis 999019c6f0 refactor: v2 return host username (#17880)
* fix: v2 return booking with host username

* auto gen docs

* auto gen docs
2024-11-27 15:15:12 +00:00
Lauris Skraucis 240ab60c5f fix: v2 return booking with split name attendee (#17877) 2024-11-27 16:22:58 +02:00
Benny Joo e8f84779f5 chore: make i18n.json the single source of truth (#17876) 2024-11-27 15:18:38 +01:00
Eunjae Lee 08a92b46f9 fix: load more rows if the existing ones do not fill the space (#17874) 2024-11-27 07:08:28 -07:00
Eunjae LeeandOmar López 877e4199e7 fix: move organization members list (#17531)
Co-authored-by: Omar López <zomars@me.com>
2024-11-27 13:53:36 +00:00
Hariom Balhara afd730e1c8 fix: (Hotfix)any group of rules in routing (#17869) 2024-11-27 10:44:45 +05:30
Morgan 471f5bd7eb chore: add Free to platform plan guard (#17862) 2024-11-27 01:40:39 +00:00
Joe Au-Yeung c676d47b7e fix: Routing form report filters (#17865) 2024-11-27 01:34:48 +00:00
GitHub Actions b3536728e0 chore: release v4.7.12
Post release / release (push) Canceled after 0s
2024-11-26 20:22:01 +00:00
Eunjae LeeandOmar López e22fec93b1 fix: provide fixed widths for DataTable columns (#17853)
Co-authored-by: Omar López <zomars@me.com>
2024-11-26 20:17:33 +00:00
Joe Au-Yeung f81716fbb2 feat: Salesforce - pass company booking response to the Lead field (#17860)
* Catch undefined app option properties

* Add `getCompanyNameFromBookingResponse` method

* Pass bookingResponse when Lead is being created

* Pass bookingResponse from CRMManager

* Handle empty company response

* Return the company value
2024-11-26 19:14:44 +00:00
Anik Dhabal Babu 0b66ddc6db fix: unable to remove banner (#17857) 2024-11-26 18:42:41 +00:00
Somay Chauhan 382a27f134 fix: round robin host reschedule error (#17855) 2024-11-26 18:08:58 +00:00
Calcom BotandReplexica ac5ee225d5 feat: update translations via @replexica (#17854)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2024-11-26 16:46:54 +00:00
Anik Dhabal Babu d8fd6f5c25 chore: exclude emails form field refactor (#17824)
* chore: exclude emails refactor

* Update
2024-11-26 21:30:28 +05:30
Benny Joo 0e333aa71c fix: use tags icon for attr (#17841) 2024-11-26 12:52:21 -03:00
Carina WollendorferandCarinaWolli ae70786b2b fix: send attendee email with event updates of reassignment (#17836)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-11-26 10:33:00 -05:00
Benny Joo 2df553b58b chore: use app router for settings/platform/members page (#17849)
* use i18n strings

* migrate platform/members page
2024-11-26 15:33:01 +01:00
Morgan d4afa1436d chore: make throttler logs verbose (#17845) 2024-11-26 09:42:27 +00:00
Morgan 88efa0a2b2 fix: oauth client users controller permissions (#17822)
* fix: oauth client users controller permissions

* fixup! fix: oauth client users controller permissions

* fixup! fixup! fix: oauth client users controller permissions

* fixup! fixup! fixup! fix: oauth client users controller permissions
2024-11-26 09:41:03 +00:00
Calcom BotandReplexica b563e41623 feat: update translations via @replexica (#17842)
Co-authored-by: Replexica <support@replexica.com>
2024-11-25 22:17:54 +00:00
Benny Joo f097f852ad chore: settings/organizations - remove pages router and use app router (#16774)
* remove pages router and use app router

* remove future/settings/organizations files

* fix billing page

* sync SettingsLayoutAppDirClient and fix members page

* fix attribute pages

* remove isAppDir where not needed anymore

* add CTA button for members view
2024-11-25 16:49:00 -05:00
Joe Au-Yeung aea8588a21 fix: Salesforce owner check, handle no contact email (#17834) 2024-11-25 19:38:06 +00:00
Benny Joo 7e0c684348 fix: UI issue in Phone Input dropdown (#17837) 2024-11-25 19:35:09 +00:00
Keith Williams 21956f096d chore: Add check to skip DB migrations as part of build (#17827) 2024-11-25 16:33:51 -03:00
Benny Joo d1adfa3bf9 fix (#17812) 2024-11-25 14:07:55 -05:00
Benny JooandKeith Williams 62976a4c77 feat: AI description - Backend for adding translation objects to EventType model (#17657)
* feat: AI description - DB model + frontend + backend (fetch only)

* fix types and add validation to backend

* improve log

* improve

* import type

* add EventTypeTranslationRepository

* fix replexica error

* fix replexica error

* fix

* fix test

* wip

* wip

* add createManyDescriptionTranslations method

* improve logic

* refactor

* update replexica type

* improve typing

* Renamed descriptionTranslations to fieldTranslations

* log errors in replexica service

* handle translations in background

* don't create, upsert instead

* make sure that description is updated

* simplify

* fix

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

* improve filter logic in frontend

* implement background job using Tasker

* update schema

* update schema again

* rename

* rename id to uid

* add migration

* fix type

* update

* update schema and migration sql

* fix migration

* fix type check

* fix type check 2

* improve find logic in EventMeta

* add more validation

* add logger

* rename

* upgrade replexica/sdk to 0.7.0

* upgrade replexica to 0.7.0

* use batchLocalizeText

* add comments

* override browser locale if user is a signed in calcom user

* fix type in replexica

* fix migration sql

* fix type

* use dynamic import

* do not use batchLocalizeText

* fix

* add more validations

* fix type error

* address comment

* update locales

* update locales

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-11-25 18:37:42 +00:00
GitHub Actions 93489709a8 chore: release v4.7.11
Post release / release (push) Canceled after 0s
2024-11-25 16:35:44 +00:00
Amit Sharma 050102fde2 fix: OOO redirect, email wrong formatting (#17829) 2024-11-25 16:11:44 +00:00
Morgan c1112ab2ab fix: extract clientId header manually apiv2 (Revert) (#17832)
This reverts commit b0bf87f3ed.
2024-11-25 15:59:39 +00:00
Morgan 34d4c3d775 fix: api v2 webhook issue (#17831) 2024-11-25 15:52:00 +00:00
Somay Chauhan e477e5892c feat: added customClassnames for event-setup tab (#17240)
* feat: added customClassnames for event-setup tab

* Update EventTypePlatformWrapper.tsx

* better classnames naming

* added classNames to common types

* updated classnames to use the common exported types

* udpate

* better classnames

* better classname names

* classnames -> classNames

* added default styling

* Update Locations.tsx

* Update event-types.tsx

* update

* locationSelectCustomClassNames -> LocationSelectCustomClassNames

* Update documentation.json

* fix: better classnames

* Update types.ts
2024-11-25 09:59:09 +00:00
ad44ca9f2a fix: change order of eventtypes and routingforms (#17615)
* change order of eventtypes and routingforms

* for workflows

* for filtered workflows

* update slug of first expected event-type

* updated due to latest merges

* test to ensure new eventtype added is first in the list

* test for workflows

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-11-25 06:25:05 -03:00
Somay ChauhanandMorgan 868cf91c35 refactor: conferencing apps atoms web wrapper (#17732)
* conferencing apps atoms web wrapper

* Update DisconnectIntegrationModal.tsx

* formating changes

* handleBulkUpdateDefaultLocation -> bulkUpdateFunction

* fix: type error for handleRemoveApp

* fix: type error for handleRemoveApp

* refactor: removed ConferencingView component and migrated its functionality to ConferencingAppsViewWebWrapper

* refactor: moved AppList, AppListCard to packages/features

* fix: type changes

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-11-25 09:53:38 +02:00
Lauris Skraucis 8b9693ea60 fix: v2 booking location (#17817)
* fix: create-booking.input dont require location to be url

* fix: return video call url for booking location

* try to debug test

* remove test file
2024-11-25 07:34:59 +00:00
Somay Chauhan 935115d7c1 feat: added custom classnames for event-limits tab (#17478)
* added custom classnames for event limits tab

* added classNames to common types

* updated classnames to use the common exported types

* undo

* Update EventTypePlatformWrapper.tsx

* better clasnames naming

* added example classnames for demo

* fix: better calssnames

bookingFrequencyLimitSection-> bookingFrequencyLimit
totalDurationLimitSection-> totalDurationLimit
futureBookingLimitSection-> futureBookingLimit
offsetStartTimesSection-> offsetStartTimes

* suffix->addOn

* Update types.ts
2024-11-25 07:13:58 +00:00
Somay Chauhan eae74c568c feat: added custom classnames for event-advanced tab (#17479)
* feat: added custom classnames to event-advanced tab

* added more calssnames

* added classNames to common types

* undo extra changes

* Update EventTypePlatformWrapper.tsx

* Update types.ts

* Update EventTypePlatformWrapper.tsx

* better classnames

* Update EventTypePlatformWrapper.tsx

* addeed CustomEventTypeModalClassNames

* added example classnames for demo and some changes

* fix: better classnames

* timezoneLockToggle -> timezoneLock

* Update types.ts

* Update CustomEventTypeModal.tsx
2024-11-25 12:11:27 +05:30
Somay Chauhan 9e9dd6070d feat: added custom classnames to event-assignment tab atom (#17527)
* added classNames to common types

* Update types.ts

* feat: added custom classnames to event-assignment tab atom

* Update EventTypePlatformWrapper.tsx

* update: better classnames

* added example clasnames for demo

* fix: classnames

* fix: type check

* fixed classnames

* fix: better calssnames

assingAllTeamMembersToggle -> assingAllTeamMembers
enableWeightsToggle -> enableWeights
assingAllTeamMembersToggle -> assingAllTeamMembers
2024-11-25 06:10:22 +01:00
Amit SharmaandPeer Richelsen 5af10a2f61 feat: past and overlapping ooo (#17373)
* feat: past and overlapping ooo

* use existing props

* added e2e test

* disallow duplicates added tests

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-11-24 18:58:22 +00:00
Peer Richelsen 3936a0ae94 target blank for settings link (#17815) 2024-11-23 21:12:44 +00:00
Morgan c2a3d8b8df fix: extract trpc and react-awesome-query-builder from atoms (#17801)
* wip

* fix: externalize react-awesome-query-builder

* fix: move useSession to webWrapper

* fixup! Merge branch 'main' into fix-platform-event-types-segment

* fixup! fixup! Merge branch 'main' into fix-platform-event-types-segment

* fixup! fixup! fixup! Merge branch 'main' into fix-platform-event-types-segment

* fixup! fixup! fixup! fixup! Merge branch 'main' into fix-platform-event-types-segment
2024-11-23 12:29:16 +00:00
GitHub Actions 9bb66035c8 chore: release v4.7.10
Post release / release (push) Canceled after 0s
2024-11-22 23:01:45 +00:00
Joe Au-Yeung 226325062e Conditionally get the first assignment reason (#17808) 2024-11-22 22:23:15 +00:00
Carina Wollendorfer b61661214a fix: improvements for canceling workflow reminders (#17807) 2024-11-22 13:55:30 -07:00
Omar López 9f2047f203 fix: Got back to access token for release action
Signed-off-by: Omar López <zomars@me.com>
2024-11-22 13:53:19 -07:00
Omar López 9b0db345b0 Revert "Add new release actions"
This reverts commit 05c8b53444.
2024-11-22 13:50:27 -07:00
GitHub Actions a84a2b3152 chore: release 4.7.9
Post release / release (push) Canceled after 0s
2024-11-22 20:43:40 +00:00
Omar López 087dbefec2 fix: add missing version plugin for release action 2024-11-22 13:35:52 -07:00
Omar López 46ba793521 fix: draft release setup 2024-11-22 13:32:29 -07:00
Omar López 21ab9bf109 fix: draft release setup 2024-11-22 13:30:15 -07:00
Omar López 05c8b53444 Add new release actions
Signed-off-by: Omar López <zomars@me.com>
2024-11-22 13:27:33 -07:00
Joe Au-Yeung e7495e311b feat: Render assignment reason on routing form report (#17791) 2024-11-22 10:57:16 -07:00
Omar López 9c10ecf8ea ci: Add new release actions (#17789) 2024-11-22 10:08:26 -07:00
Lauris Skraucis 9fa73aca86 fix: v2 booking confirmation emails not sent (#17764)
* fix: round robin recurring don't send email if noEmail

* fix: noEmail: false for non platform requests

* fix: booking e2e tests

* refactor: move bookings e2e tests in folder and uniform name

* test: emails

* test: team emails

* reschedule and cancel emails with api key

* test: round robin reassign emails

* test: confirm and decline emails

* test: confirm and decline emails

* fix tests

* test: recurring booking emails

* remove unused imports and spies

* fix test

* remove log

* chore: bump libraries

* try to fix ci tests
2024-11-22 18:19:56 +02:00
Alex van Andel 8e7ce1216c Revert "feat: install gcal + gvideo by default for google signups (#17773)" (#17803)
release.yml / release (push) Canceled after 0s
This reverts commit fcd06c80e3.
2024-11-22 15:55:11 +00:00
Alex van Andel 4ddbdde553 chore: Release v4.7.8 2024-11-22 15:54:42 +00:00
Keith Williams cd24067f17 chore: Add ability to disable Sentry source maps (#17699)
* chore: Remove Sentry source maps

* Using .env to control Sentry source maps

* Trying with sourcemaps property

* Turned silent off for testing

* Completely turned off devtool
2024-11-22 15:19:52 +00:00
Alex van Andel 040adb2ddf chore: bump lockfile (#17802) 2024-11-22 15:14:39 +00:00
Somay Chauhan 4d578d2cdf feat: added customClassnames to event-availability tab (#17268)
* feat: added customClassnames to event-availability tab

* added classNames to common types

* use common classnames type

* Update types.ts

* update name

* fix: better classnames

* better calssnames

* Update types.ts
2024-11-22 10:35:20 +01:00
Anwar Sadath 1cf84a270c feat: Expose booking created at in v2 API response (#17452)
* feat: Expose booking created at in booking detailed view and v2 API response

* Fix tests

* Fix spacing

* Add API v2 tests to verify createdAt is present while creating a booking and fetching a booking

* Add test to validate createdAt after rescheduling a booking
2024-11-22 13:50:11 +05:30
Somay Chauhan d03651312e feat: added customClassnames for event-recurring tab (#17551)
* added classNames to common types

* Update types.ts

* feat: added custom classnames to event-recurring tab

* Update EventTypePlatformWrapper.tsx

* customClassnames -> customClassNames

* added example classnames for demo

* fixed classnames
2024-11-22 12:30:02 +05:30
504 changed files with 13432 additions and 9550 deletions
+4 -5
View File
@@ -119,7 +119,7 @@ NEXT_PUBLIC_FRESHCHAT_HOST=
# Google OAuth credentials
# To enable Login with Google you need to:
# 1. Set `GOOGLE_API_CREDENTIALS` below
# 1. Set `GOOGLE_API_CREDENTIALS` above
# 2. Set `GOOGLE_LOGIN_ENABLED` to `true`
# When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications
@@ -147,6 +147,8 @@ NEXT_PUBLIC_SENDGRID_SENDER_NAME=
# Used for capturing exceptions and logging messages
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_DEBUG=
SENTRY_DISABLE_CLIENT_SOURCE_MAPS=
SENTRY_DISABLE_SERVER_SOURCE_MAPS=
SENTRY_MAX_SPANS=
SENTRY_TRACES_SAMPLE_RATE=
@@ -345,8 +347,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
AB_TEST_BUCKET_PROBABILITY=50
# whether we redirect to the future/event-types from event-types or not
APP_ROUTER_EVENT_TYPES_ENABLED=0
APP_ROUTER_SETTINGS_DEVELOPER_ENABLED=0
APP_ROUTER_SETTINGS_ORG_ENABLED=0
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
APP_ROUTER_APPS_SLUG_ENABLED=0
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
@@ -357,7 +357,6 @@ APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
APP_ROUTER_BOOKING_ENABLED=0
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
APP_ROUTER_WORKFLOWS_ENABLED=0
APP_ROUTER_SETTINGS_TEAMS_ENABLED=0
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
APP_ROUTER_APPS_ENABLED=0
APP_ROUTER_VIDEO_ENABLED=0
@@ -414,4 +413,4 @@ NEXT_PUBLIC_WEBSITE_TERMS_URL=
NEXT_PUBLIC_LOGGER_LEVEL=
# Used to use Replexica SDK, a tool for real-time AI-powered localization
REPLEXICA_API_KEY=
REPLEXICA_API_KEY=
+50
View File
@@ -0,0 +1,50 @@
name: Draft release
run-name: Draft release ${{ inputs.next_version }}
on:
workflow_dispatch:
inputs:
next_version:
required: true
type: string
description: 'Version name'
permissions:
contents: write
jobs:
draft_release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: 'main'
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Configure git
run: |
git config --local user.email "github-actions@github.com"
git config --local user.name "GitHub Actions"
- uses: ./.github/actions/yarn-install
- name: Bump version
run: |
cd apps/web
yarn version ${{ inputs.next_version }}
- name: Commit changes
run: |
git add .
git commit -m "chore: release v${{ inputs.next_version }}"
git push
- name: Draft release
run: gh release create v$VERSION --generate-notes --draft
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.next_version }}
+1
View File
@@ -17,6 +17,7 @@ env:
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NODE_OPTIONS: --max-old-space-size=29000
REDIS_URL: "redis://localhost:6379"
REPLEXICA_API_KEY: ${{ secrets.CI_REPLEXICA_API_KEY }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
+25
View File
@@ -0,0 +1,25 @@
name: Post release
on:
workflow_dispatch:
push:
# Pattern matched against refs/tags
tags:
- "*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: 'main'
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Configure git
run: |
git config --local user.email "github-actions@github.com"
git config --local user.name "GitHub Actions"
- run: git push origin +main:production
-17
View File
@@ -1,17 +0,0 @@
on:
workflow_dispatch:
push:
# Pattern matched against refs/tags
tags:
- "*" # Push events to every tag not containing /
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: main # Always checkout main even for tagged releases
fetch-depth: 0
token: ${{ secrets.GH_ACCESS_TOKEN }}
- run: git push origin +main:production
+2
View File
@@ -1,6 +1,8 @@
name: Unit
on:
workflow_call:
env:
REPLEXICA_API_KEY: ${{ secrets.CI_REPLEXICA_API_KEY }}
permissions:
contents: read
jobs:
File diff suppressed because one or more lines are too long
View File
View File
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
View File
View File
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
View File
View File
+2
View File
@@ -5,5 +5,7 @@ plugins:
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: "@yarnpkg/plugin-version"
yarnPath: .yarn/releases/yarn-3.4.1.cjs
+22 -9
View File
@@ -6,6 +6,7 @@ import { IS_PRODUCTION } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { isAdminGuard } from "../utils/isAdmin";
import { isLockedOrBlocked } from "../utils/isLockedOrBlocked";
import { ScopeOfAdmin } from "../utils/scopeOfAdmin";
// Used to check if the apiKey is not expired, could be extracted if reused. but not for now.
@@ -24,23 +25,35 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => {
if (!hasValidLicense && IS_PRODUCTION) {
return res.status(401).json({ error: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
}
// Check if the apiKey query param is provided.
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
// remove the prefix from the user provided api_key. If no env set default to "cal_"
const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", "");
// Hash the key again before matching against the database records.
const hashedKey = hashAPIKey(strippedApiKey);
// Check if the hashed api key exists in database.
const apiKey = await prisma.apiKey.findUnique({ where: { hashedKey } });
// If cannot find any api key. Throw a 401 Unauthorized.
if (!apiKey) return res.status(401).json({ error: "Your apiKey is not valid" });
const apiKey = await prisma.apiKey.findUnique({
where: { hashedKey },
include: {
user: {
select: { role: true, locked: true, email: true },
},
},
});
if (!apiKey) return res.status(401).json({ error: "Your API key is not valid." });
if (apiKey.expiresAt && dateNotInPast(apiKey.expiresAt)) {
return res.status(401).json({ error: "This apiKey is expired" });
return res.status(401).json({ error: "This API key is expired." });
}
if (!apiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" });
if (!apiKey.userId || !apiKey.user)
return res.status(404).json({ error: "No user found for this API key." });
// save the user id in the request for later use
req.userId = apiKey.userId;
req.user = apiKey.user;
const { isAdmin, scope } = await isAdminGuard(req);
const userIsLockedOrBlocked = await isLockedOrBlocked(req);
if (userIsLockedOrBlocked)
return res.status(403).json({ error: "You are not authorized to perform this request." });
req.isSystemWideAdmin = isAdmin && scope === ScopeOfAdmin.SystemWide;
req.isOrganizationOwnerOrAdmin = isAdmin && scope === ScopeOfAdmin.OrgOwnerOrAdmin;
+3 -4
View File
@@ -6,8 +6,7 @@ import { UserPermissionRole, MembershipRole } from "@calcom/prisma/enums";
import { ScopeOfAdmin } from "./scopeOfAdmin";
export const isAdminGuard = async (req: NextApiRequest) => {
const { userId } = req;
const user = await prisma.user.findUnique({ where: { id: userId }, select: { role: true } });
const { user, userId } = req;
if (!user) return { isAdmin: false, scope: null };
const { role: userRole } = user;
@@ -34,7 +33,7 @@ export const isAdminGuard = async (req: NextApiRequest) => {
},
},
});
if (!orgOwnerOrAdminMemberships.length) return { isAdmin: false, scope: null };
if (orgOwnerOrAdminMemberships.length > 0) return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin };
return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin };
return { isAdmin: false, scope: null };
};
@@ -0,0 +1,9 @@
import type { NextApiRequest } from "next";
import { checkIfEmailInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
export async function isLockedOrBlocked(req: NextApiRequest) {
const user = req.user;
if (!user?.email) return false;
return user.locked || (await checkIfEmailInWatchlistController(user.email));
}
+1
View File
@@ -8,6 +8,7 @@ export declare module "next" {
session?: Session | null;
userId: number;
user?: { role: string; locked: boolean; email: string } | null;
method: string;
// session: { user: { id: number } };
// query: Partial<{ [key: string]: string | string[] }>;
@@ -1,17 +1,17 @@
import prismaMock from "../../../../../../tests/libs/__mocks__/prismaMock";
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, vi, it, expect, afterEach, beforeEach } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
import prisma from "@calcom/prisma";
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
import { isAdminGuard } from "~/lib/utils/isAdmin";
import { hashAPIKey } from "~/../../../packages/features/ee/api-keys/lib/apiKeys";
import { verifyApiKey } from "../../../lib/helpers/verifyApiKey";
import { ScopeOfAdmin } from "../../../lib/utils/scopeOfAdmin";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
@@ -20,11 +20,6 @@ afterEach(() => {
vi.resetAllMocks();
});
vi.mock("@calcom/prisma");
vi.mock("~/lib/utils/isAdmin", () => ({
isAdminGuard: vi.fn(),
}));
describe("Verify API key", () => {
let service: LicenseKeyService;
@@ -45,7 +40,6 @@ describe("Verify API key", () => {
};
vi.mocked(service.checkLicense).mockResolvedValue(false);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null });
const serverNext = vi.fn((next: void) => Promise.resolve(next));
@@ -69,7 +63,6 @@ describe("Verify API key", () => {
};
vi.mocked(service.checkLicense).mockResolvedValue(true);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null });
const serverNext = vi.fn((next: void) => Promise.resolve(next));
@@ -91,10 +84,18 @@ describe("Verify API key", () => {
},
prisma,
});
prismaMock.apiKey.findUnique.mockResolvedValue({
id: 1,
userId: 2,
const hashedKey = hashAPIKey("test_key");
await prismock.apiKey.create({
data: {
hashedKey,
user: {
create: {
email: "admin@example.com",
role: UserPermissionRole.ADMIN,
locked: false,
},
},
},
});
const middleware = {
@@ -102,7 +103,6 @@ describe("Verify API key", () => {
};
vi.mocked(service.checkLicense).mockResolvedValue(true);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.SystemWide });
const serverNext = vi.fn((next: void) => Promise.resolve(next));
@@ -125,10 +125,36 @@ describe("Verify API key", () => {
},
prisma,
});
prismaMock.apiKey.findUnique.mockResolvedValue({
id: 1,
userId: 2,
const hashedKey = hashAPIKey("test_key");
await prismock.apiKey.create({
data: {
hashedKey,
user: {
create: {
email: "org-admin@acme.com",
role: UserPermissionRole.USER,
locked: false,
teams: {
create: {
accepted: true,
role: MembershipRole.OWNER,
team: {
create: {
name: "ACME",
isOrganization: true,
organizationSettings: {
create: {
isAdminAPIEnabled: true,
orgAutoAcceptEmail: "acme.com",
},
},
},
},
},
},
},
},
},
});
const middleware = {
@@ -136,7 +162,6 @@ describe("Verify API key", () => {
};
vi.mocked(service.checkLicense).mockResolvedValue(true);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin });
const serverNext = vi.fn((next: void) => Promise.resolve(next));
@@ -149,4 +174,44 @@ describe("Verify API key", () => {
expect(req.isSystemWideAdmin).toBe(false);
expect(req.isOrganizationOwnerOrAdmin).toBe(true);
});
it("should return 403 if user is locked or blocked", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {},
query: {
apiKey: "cal_test_key",
},
prisma,
});
const hashedKey = hashAPIKey("test_key");
await prismock.apiKey.create({
data: {
hashedKey,
user: {
create: {
email: "locked@example.com",
role: UserPermissionRole.USER,
locked: true,
},
},
},
});
const middleware = {
fn: verifyApiKey,
};
vi.mocked(service.checkLicense).mockResolvedValue(true);
const serverNext = vi.fn((next: void) => Promise.resolve(next));
const middlewareSpy = vi.spyOn(middleware, "fn");
await middleware.fn(req, res, serverNext);
expect(middlewareSpy).toBeCalled();
expect(res.statusCode).toBe(403);
expect(JSON.parse(res._getData())).toEqual({ error: "You are not authorized to perform this request." });
expect(serverNext).not.toHaveBeenCalled();
});
});
@@ -19,6 +19,7 @@ describe("isAdmin guard", () => {
});
req.userId = 0;
req.user = undefined;
const { isAdmin, scope } = await isAdminGuard(req);
@@ -35,6 +36,7 @@ describe("isAdmin guard", () => {
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
req.userId = memberUser.id;
req.user = memberUser;
const { isAdmin, scope } = await isAdminGuard(req);
@@ -51,6 +53,7 @@ describe("isAdmin guard", () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "admin@example.com" } });
req.userId = adminUser.id;
req.user = adminUser;
const { isAdmin, scope } = await isAdminGuard(req);
@@ -67,6 +70,8 @@ describe("isAdmin guard", () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
req.userId = adminUser.id;
req.user = adminUser;
const { isAdmin, scope } = await isAdminGuard(req);
expect(isAdmin).toBe(true);
expect(scope).toBe(ScopeOfAdmin.OrgOwnerOrAdmin);
@@ -81,6 +86,8 @@ describe("isAdmin guard", () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-dunder@example.com" } });
req.userId = adminUser.id;
req.user = adminUser;
const { isAdmin } = await isAdminGuard(req);
expect(isAdmin).toBe(false);
});
@@ -0,0 +1,88 @@
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it, beforeEach } from "vitest";
import { isLockedOrBlocked } from "../../../lib/utils/isLockedOrBlocked";
describe("isLockedOrBlocked", () => {
beforeEach(async () => {
await prismock.watchlist.createMany({
data: [
{
type: "DOMAIN",
value: "spam.com",
createdById: 1,
},
{
type: "DOMAIN",
value: "blocked.com",
createdById: 1,
},
],
});
});
it("should return false if no user in request", async () => {
const req = { userId: null, user: null } as any;
const result = await isLockedOrBlocked(req);
expect(result).toBe(false);
});
it("should return false if user has no email", async () => {
const req = { userId: 123, user: { email: null } } as any;
const result = await isLockedOrBlocked(req);
expect(result).toBe(false);
});
it("should return true if user is locked", async () => {
const req = {
userId: 123,
user: {
locked: true,
email: "test@example.com",
},
} as any;
const result = await isLockedOrBlocked(req);
expect(result).toBe(true);
});
it("should return true if user email domain is watchlisted", async () => {
const req = {
userId: 123,
user: {
locked: false,
email: "test@blocked.com",
},
} as any;
const result = await isLockedOrBlocked(req);
expect(result).toBe(true);
});
it("should return false if user is not locked and email domain is not watchlisted", async () => {
const req = {
userId: 123,
user: {
locked: false,
email: "test@example.com",
},
} as any;
const result = await isLockedOrBlocked(req);
expect(result).toBe(false);
});
it("should handle email domains case-insensitively", async () => {
const req = {
userId: 123,
user: {
locked: false,
email: "test@BLOCKED.COM",
},
} as any;
const result = await isLockedOrBlocked(req);
expect(result).toBe(true);
});
});
+1
View File
@@ -12,6 +12,7 @@
},
"include": [
"next-env.d.ts",
"./next.d.ts",
"**/*.ts",
"**/*.tsx",
"../../../packages/types/*.d.ts",
+1 -1
View File
@@ -29,7 +29,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.62",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.69",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -160,10 +160,10 @@ export class BookingsController_2024_04_15 {
@Post("/")
async createBooking(
@Req() req: BookingRequest,
@Body() body: CreateBookingInput_2024_04_15
@Body() body: CreateBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse<Partial<BookingResponse>>> {
const clientIdHeader = req.get(X_CAL_CLIENT_ID);
const oAuthClientId = clientIdHeader?.toString();
const oAuthClientId = clientId?.toString();
const { orgSlug, locationUrl } = body;
req.headers["x-cal-force-slug"] = orgSlug;
try {
@@ -191,10 +191,10 @@ export class BookingsController_2024_04_15 {
async cancelBooking(
@Req() req: BookingRequest,
@Param("bookingId") bookingId: string,
@Body() _: CancelBookingInput_2024_04_15
@Body() _: CancelBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
const clientIdHeader = req.get(X_CAL_CLIENT_ID);
const oAuthClientId = clientIdHeader?.toString();
const oAuthClientId = clientId?.toString();
if (bookingId) {
try {
req.body.id = parseInt(bookingId);
@@ -245,10 +245,10 @@ export class BookingsController_2024_04_15 {
@Post("/recurring")
async createRecurringBooking(
@Req() req: BookingRequest,
@Body() _: CreateRecurringBookingInput_2024_04_15[]
@Body() _: CreateRecurringBookingInput_2024_04_15[],
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse<BookingResponse[]>> {
const clientIdHeader = req.get(X_CAL_CLIENT_ID);
const oAuthClientId = clientIdHeader?.toString();
const oAuthClientId = clientId?.toString();
try {
const recurringEventId = uuidv4();
for (const recurringEvent of req.body) {
@@ -283,10 +283,10 @@ export class BookingsController_2024_04_15 {
@Post("/instant")
async createInstantBooking(
@Req() req: BookingRequest,
@Body() _: CreateBookingInput_2024_04_15
@Body() _: CreateBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
const clientIdHeader = req.get(X_CAL_CLIENT_ID);
const oAuthClientId = clientIdHeader?.toString();
const oAuthClientId = clientId?.toString();
req.userId = (await this.getOwnerId(req)) ?? -1;
try {
const instantMeeting = await handleInstantMeeting(
@@ -333,7 +333,7 @@ export class BookingsController_2024_04_15 {
}
private async getOAuthClientsParams(clientId: string): Promise<OAuthRequestParams> {
const res = DEFAULT_PLATFORM_PARAMS;
const res = { ...DEFAULT_PLATFORM_PARAMS };
try {
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
// fetch oAuthClient from db and use data stored in db to set these values
@@ -1,160 +0,0 @@
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 { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.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 { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
describe("With api key", () => {
let app: INestApplication;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let apiKeyString: string;
const userEmail = "bookings-controller-e2e@api.com";
let user: User;
let eventTypeId: 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);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
user = await userRepositoryFixture.create({
email: userEmail,
});
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = keyString;
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
user.id
);
eventTypeId = event.id;
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
});
describe("create bookings", () => {
it("should create a booking with api key", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId,
attendee: {
name: "Mr Key",
email: "mr_key@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
meetingUrl: "https://meet.google.com/abc-def-ghi",
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.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(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
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.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.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(user.email);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
await app.close();
});
});
});
@@ -0,0 +1,291 @@
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 { 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";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.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 { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
AttendeeScheduledEmail,
OrganizerScheduledEmail,
AttendeeRescheduledEmail,
OrganizerRescheduledEmail,
AttendeeCancelledEmail,
OrganizerCancelledEmail,
} from "@calcom/platform-libraries";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
CancelBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
jest.spyOn(AttendeeScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
jest.spyOn(OrganizerScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
jest
.spyOn(AttendeeRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
describe("Bookings Endpoints 2024-08-13", () => {
describe("With api key", () => {
let app: INestApplication;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let apiKeyString: string;
const userEmail = "bookings-controller-e2e@api.com";
let user: User;
let eventTypeId: number;
let createdBooking: BookingOutput_2024_08_13;
let rescheduledBooking: BookingOutput_2024_08_13;
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);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
user = await userRepositoryFixture.create({
email: userEmail,
});
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = keyString;
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
user.id
);
eventTypeId = event.id;
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
});
it("should create a booking with api key", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId,
attendee: {
name: "Mr Key",
email: "mr_key@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
meetingUrl: "https://meet.google.com/abc-def-ghi",
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.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(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
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.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.absentHost).toEqual(false);
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
createdBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should reschedule a booking with api key", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${createdBooking.uid}/reschedule`)
.send(body)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 15, 0, 0)).toISOString());
expect(data.rescheduledFromUid).toEqual(createdBooking.uid);
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual(createdBooking.status);
expect(data.duration).toEqual(createdBooking.duration);
expect(data.eventTypeId).toEqual(createdBooking.eventTypeId);
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
expect(data.location).toEqual(createdBooking.location);
expect(data.absentHost).toEqual(createdBooking.absentHost);
expect(data.metadata).toEqual(createdBooking.metadata);
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
rescheduledBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should cancel booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
const booking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid);
expect(booking).toBeDefined();
expect(booking?.status).toEqual("ACCEPTED");
return request(app.getHttpServer())
.post(`/v2/bookings/${rescheduledBooking.uid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("cancelled");
expect(data.cancellationReason).toEqual(body.cancellationReason);
expect(data.start).toEqual(rescheduledBooking.start);
expect(data.end).toEqual(rescheduledBooking.end);
expect(data.duration).toEqual(rescheduledBooking.duration);
expect(data.eventTypeId).toEqual(rescheduledBooking.eventTypeId);
expect(data.attendees[0]).toEqual(rescheduledBooking.attendees[0]);
expect(data.location).toEqual(rescheduledBooking.location);
expect(data.absentHost).toEqual(rescheduledBooking.absentHost);
const cancelledBooking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid);
expect(cancelledBooking).toBeDefined();
expect(cancelledBooking?.status).toEqual("CANCELLED");
expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
});
});
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(user.email);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
await app.close();
});
});
});
@@ -0,0 +1,526 @@
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 { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomNumber } from "test/utils/randomNumber";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
CreateBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { BookingOutput_2024_08_13 } from "@calcom/platform-types";
import { Booking, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
const suffix = randomNumber();
describe("Bookings Endpoints 2024-08-13", () => {
describe("Booking fields", () => {
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 = `alice-${suffix}@api.com`;
let user: User;
let bookingWithSplitName: Booking;
const splitName = {
firstName: "Oldie",
lastName: "Goldie",
};
let seatedBookingWithSplitName: Booking;
let eventTypeWithBookingFieldsId: 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: `booking fields ${suffix}` });
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: `normal-booking-${randomNumber()}`, length: 60 },
user.id
);
bookingWithSplitName = await bookingsRepositoryFixture.create({
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2020, 0, 8, 12, 0, 0)),
endTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
title: "peer coding lets goo",
uid: "booking",
eventType: {
connect: {
id: event.id,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: splitName,
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie Goldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const seatedEvent = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: `seated-${randomNumber()}`, length: 60, seatsPerTimeSlot: 3 },
user.id
);
seatedBookingWithSplitName = await bookingsRepositoryFixture.create({
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)),
endTime: new Date(Date.UTC(2020, 0, 8, 15, 0, 0)),
title: "peer coding lets goo",
uid: "seated-booking",
eventType: {
connect: {
id: seatedEvent.id,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: splitName,
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie Goldie",
locale: "lv",
timeZone: "Europe/Rome",
bookingSeat: {
create: {
referenceUid: "unique-seat-uid",
data: {
responses: {
email: "oldie@gmail.com",
name: {
firstName: "Oldie",
lastName: "Goldie",
},
},
},
metadata: {},
booking: {
connect: {
uid: "seated-booking",
},
},
},
},
},
},
});
const eventTypeWithBookingFields = await eventTypesRepositoryFixture.create(
{
title: "peer coding with booking fields",
slug: `with-custom-booking-fields-${randomNumber()}`,
length: 60,
bookingFields: [
{
name: "name",
type: "name",
label: "",
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
variant: "fullName",
editable: "system",
required: true,
placeholder: "",
defaultLabel: "your_name",
variantsConfig: {
variants: {
fullName: {
fields: [
{
name: "fullName",
type: "text",
label: "your_name",
required: true,
placeholder: "",
},
],
},
firstAndLastName: {
fields: [
{
name: "firstName",
type: "text",
label: "name",
required: true,
placeholder: "lauris",
},
{
name: "lastName",
type: "text",
label: "surname",
required: true,
placeholder: "skraucis",
},
],
},
},
},
disableOnPrefill: false,
},
{
name: "email",
type: "email",
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system",
required: true,
defaultLabel: "email_address",
},
{
name: "location",
type: "radioInput",
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system",
required: false,
defaultLabel: "location",
getOptionsAt: "locations",
optionsInputs: {
phone: {
type: "phone",
required: true,
placeholder: "",
},
somewhereElse: {
type: "text",
required: true,
placeholder: "",
},
attendeeInPerson: {
type: "address",
required: true,
placeholder: "",
},
},
hideWhenJustOneOption: true,
},
{
name: "title",
type: "text",
hidden: true,
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system-but-optional",
required: true,
defaultLabel: "what_is_this_meeting_about",
defaultPlaceholder: "",
},
{
name: "notes",
type: "textarea",
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
{
name: "guests",
type: "multiemail",
hidden: false,
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_guests",
defaultPlaceholder: "email",
},
{
name: "rescheduleReason",
type: "textarea",
views: [
{
id: "reschedule",
label: "Reschedule View",
},
],
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system-but-optional",
required: false,
defaultLabel: "reason_for_reschedule",
defaultPlaceholder: "reschedule_placeholder",
},
{
name: "favorite-movie",
type: "text",
label: "favorite movie",
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: true,
placeholder: "matrix",
disableOnPrefill: false,
},
],
},
user.id
);
eventTypeWithBookingFieldsId = eventTypeWithBookingFields.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;
}
describe("get individual booking", () => {
it("should should get a seated booking with split name responses", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${bookingWithSplitName.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.attendees[0].name).toEqual(`${splitName.firstName} ${splitName.lastName}`);
expect(data.bookingFieldsResponses.name).toEqual(splitName);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should should get a seated booking with split name responses", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${seatedBookingWithSplitName.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
// eslint-disable-next-line
// @ts-ignore
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
console.log("asap data", JSON.stringify(data, null, 2));
expect(data.attendees[0].name).toEqual(`${splitName.firstName} ${splitName.lastName}`);
expect(data.attendees[0].bookingFieldsResponses.name).toEqual(splitName);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
describe("make booking", () => {
it("should not be able to book an event type with custom required booking fields if they are missing in bookingFieldsResponses", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithBookingFieldsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
};
return request(app.getHttpServer())
.post(`/v2/bookings`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
it("should be able to book an event type with custom required booking fields", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithBookingFieldsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
"favorite-movie": "lord of the rings",
},
};
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(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.bookingFieldsResponses["favorite-movie"]).toEqual("lord of the rings");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
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;
}
});
@@ -1,6 +1,5 @@
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 { 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";
@@ -20,26 +19,9 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
AttendeeScheduledEmail,
OrganizerScheduledEmail,
AttendeeDeclinedEmail,
} from "@calcom/platform-libraries";
import { BookingOutput_2024_08_13, GetBookingOutput_2024_08_13 } from "@calcom/platform-types";
import { Booking, PlatformOAuthClient, Team } from "@calcom/prisma/client";
jest.spyOn(AttendeeScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
jest.spyOn(OrganizerScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
jest.spyOn(AttendeeDeclinedEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
describe("Bookings Endpoints 2024-08-13", () => {
describe("Bookings confirmation", () => {
let app: INestApplication;
@@ -87,6 +69,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -0,0 +1,474 @@
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 { 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";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.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 { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
OrganizerScheduledEmail,
AttendeeScheduledEmail,
OrganizerRescheduledEmail,
AttendeeRescheduledEmail,
OrganizerCancelledEmail,
AttendeeCancelledEmail,
AttendeeRequestEmail,
OrganizerRequestEmail,
AttendeeDeclinedEmail,
} from "@calcom/platform-libraries";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { CancelBookingInput_2024_08_13 } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
jest
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeRequestEmail.prototype, "getHtmlRequestEmail")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerRequestEmail.prototype, "getHtmlRequestEmail")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeDeclinedEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
type EmailSetup = {
user: User;
eventTypeId: number;
createdBookingUid: string;
rescheduledBookingUid: string;
};
describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
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 teamRepositoryFixture: TeamRepositoryFixture;
let emailsEnabledSetup: EmailSetup;
let emailsDisabledSetup: EmailSetup;
const authEmail = `admin-${Math.floor(Math.random() * 1000)}@example.com`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
authEmail,
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" });
await setupEnabledEmails();
await setupDisabledEmails();
await userRepositoryFixture.create({
email: authEmail,
organization: {
connect: {
id: organization.id,
},
},
role: "ADMIN",
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function setupEnabledEmails() {
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
const user = await userRepositoryFixture.create({
email: `alice-${Math.floor(Math.random() * 1000)}@gmail.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60, requiresConfirmation: true },
user.id
);
emailsEnabledSetup = {
user,
eventTypeId: event.id,
createdBookingUid: "",
rescheduledBookingUid: "",
};
}
async function setupDisabledEmails() {
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
const user = await userRepositoryFixture.create({
email: `bob-${Math.floor(Math.random() * 1000)}@gmail.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60, requiresConfirmation: true },
user.id
);
emailsDisabledSetup = {
user,
eventTypeId: event.id,
createdBookingUid: "",
rescheduledBookingUid: "",
};
}
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
areEmailsEnabled: emailsEnabled,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
describe("OAuth client managed user bookings - emails disabled", () => {
beforeEach(async () => {
jest.clearAllMocks();
});
it("should not send an email when creating a booking that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: emailsDisabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
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);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should not send an email when confirming a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.createdBookingUid}/confirm`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
it("should not send an email when creating a booking that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
eventTypeId: emailsDisabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
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);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should not send an email when declining a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.createdBookingUid}/decline`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeDeclinedEmail.prototype.getHtml).not.toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
});
describe("OAuth client managed user bookings - emails enabled", () => {
beforeEach(async () => {
jest.clearAllMocks();
});
it("should send an email when creating a booking that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
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);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should send an email when confirming a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/confirm`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
it("should send an email when creating a booking that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
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);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should send an email when declining a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/decline`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeDeclinedEmail.prototype.getHtml).toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
});
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.user.email);
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.user.email);
await userRepositoryFixture.deleteByEmail(authEmail);
await bookingsRepositoryFixture.deleteAllBookings(
emailsEnabledSetup.user.id,
emailsEnabledSetup.user.email
);
await bookingsRepositoryFixture.deleteAllBookings(
emailsDisabledSetup.user.id,
emailsDisabledSetup.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;
}
});
@@ -0,0 +1,986 @@
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 { 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";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { createParamDecorator, 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 { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
OrganizerScheduledEmail,
AttendeeScheduledEmail,
OrganizerRescheduledEmail,
AttendeeRescheduledEmail,
OrganizerCancelledEmail,
AttendeeCancelledEmail,
OrganizerReassignedEmail,
AttendeeUpdatedEmail,
} from "@calcom/platform-libraries";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { CancelBookingInput_2024_08_13 } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
jest
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerReassignedEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeUpdatedEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
type EmailSetup = {
team: Team;
member1: User;
member2: User;
collectiveEventType: {
id: number;
createdBookingUid: string;
rescheduledBookingUid: string;
};
roundRobinEventType: {
id: number;
createdBookingUid: string;
rescheduledBookingUid: string;
currentHostId: number;
};
};
describe("Bookings Endpoints 2024-08-13 team emails", () => {
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 teamRepositoryFixture: TeamRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let hostsRepositoryFixture: HostsRepositoryFixture;
let emailsEnabledSetup: EmailSetup;
let emailsDisabledSetup: EmailSetup;
const authEmail = "admin@example.com";
beforeAll(async () => {
const moduleRef = await withApiAuth(
authEmail,
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);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
await setupEnabledEmails();
await setupDisabledEmails();
await userRepositoryFixture.create({
email: authEmail,
organization: {
connect: {
id: organization.id,
},
},
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function setupEnabledEmails() {
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
const team = await teamRepositoryFixture.create({
name: "team 1",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
connect: {
id: oAuthClientEmailsEnabled.id,
},
},
});
const member1 = await userRepositoryFixture.create({
email: "alice@gmail.com",
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
},
},
});
const member2 = await userRepositoryFixture.create({
email: "bob@gmail.com",
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(member1.id, userSchedule);
await schedulesService.createUserSchedule(member2.id, userSchedule);
await profileRepositoryFixture.create({
uid: `usr-${member1.id}`,
username: member1.email,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: member1.id,
},
},
});
await profileRepositoryFixture.create({
uid: `usr-${member2.id}`,
username: member2.email,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: member2.id,
},
},
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: member1.id } },
team: { connect: { id: team.id } },
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: member2.id } },
team: { connect: { id: team.id } },
accepted: true,
});
const collectiveEvent = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
team: {
connect: { id: team.id },
},
title: "Collective Event Type",
slug: "collective-event-type",
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
});
await hostsRepositoryFixture.create({
isFixed: true,
user: {
connect: {
id: member1.id,
},
},
eventType: {
connect: {
id: collectiveEvent.id,
},
},
});
await hostsRepositoryFixture.create({
isFixed: true,
user: {
connect: {
id: member2.id,
},
},
eventType: {
connect: {
id: collectiveEvent.id,
},
},
});
const roundRobinEvent = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "ROUND_ROBIN",
team: {
connect: { id: team.id },
},
title: "Round Robin Event Type",
slug: "round-robin-event-type",
length: 60,
assignAllTeamMembers: false,
bookingFields: [],
locations: [],
});
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: member1.id,
},
},
eventType: {
connect: {
id: roundRobinEvent.id,
},
},
});
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: member2.id,
},
},
eventType: {
connect: {
id: roundRobinEvent.id,
},
},
});
emailsEnabledSetup = {
team,
member1: member1,
member2: member2,
collectiveEventType: {
id: collectiveEvent.id,
createdBookingUid: "",
rescheduledBookingUid: "",
},
roundRobinEventType: {
id: roundRobinEvent.id,
createdBookingUid: "",
rescheduledBookingUid: "",
currentHostId: 0,
},
};
}
async function setupDisabledEmails() {
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
const team = await teamRepositoryFixture.create({
name: "team 2",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
connect: {
id: oAuthClientEmailsDisabled.id,
},
},
});
const member1 = await userRepositoryFixture.create({
email: "charlie@gmail.com",
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
},
},
});
const member2 = await userRepositoryFixture.create({
email: "dean@gmail.com",
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(member1.id, userSchedule);
await schedulesService.createUserSchedule(member2.id, userSchedule);
await profileRepositoryFixture.create({
uid: `usr-${member1.id}`,
username: member1.email,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: member1.id,
},
},
});
await profileRepositoryFixture.create({
uid: `usr-${member2.id}`,
username: member2.email,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: member2.id,
},
},
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: member1.id } },
team: { connect: { id: team.id } },
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: member2.id } },
team: { connect: { id: team.id } },
accepted: true,
});
const collectiveEvent = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
team: {
connect: { id: team.id },
},
title: "Collective Event Type",
slug: "collective-event-type",
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
});
await hostsRepositoryFixture.create({
isFixed: true,
user: {
connect: {
id: member1.id,
},
},
eventType: {
connect: {
id: collectiveEvent.id,
},
},
});
await hostsRepositoryFixture.create({
isFixed: true,
user: {
connect: {
id: member2.id,
},
},
eventType: {
connect: {
id: collectiveEvent.id,
},
},
});
const roundRobinEvent = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "ROUND_ROBIN",
team: {
connect: { id: team.id },
},
title: "Round Robin Event Type",
slug: "round-robin-event-type",
length: 60,
assignAllTeamMembers: false,
bookingFields: [],
locations: [],
});
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: member1.id,
},
},
eventType: {
connect: {
id: roundRobinEvent.id,
},
},
});
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: member2.id,
},
},
eventType: {
connect: {
id: roundRobinEvent.id,
},
},
});
emailsDisabledSetup = {
team,
member1: member1,
member2: member2,
collectiveEventType: {
id: collectiveEvent.id,
createdBookingUid: "",
rescheduledBookingUid: "",
},
roundRobinEventType: {
id: roundRobinEvent.id,
createdBookingUid: "",
rescheduledBookingUid: "",
currentHostId: 0,
},
};
}
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
areEmailsEnabled: emailsEnabled,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
beforeEach(async () => {
jest.clearAllMocks();
});
describe("OAuth client team bookings - emails disabled", () => {
describe("book", () => {
it("should not send an email when booking collective event", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
eventTypeId: emailsDisabledSetup.collectiveEventType.id,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
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);
if (responseDataIsBooking(responseBody.data)) {
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.collectiveEventType.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should not send an email when booking round robin event", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
eventTypeId: emailsDisabledSetup.roundRobinEventType.id,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
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);
if (responseDataIsBooking(responseBody.data)) {
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.roundRobinEventType.createdBookingUid = responseBody.data.uid;
emailsDisabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
describe("reschedule", () => {
it("should not send an email when rescheduling collective booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 11, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.collectiveEventType.createdBookingUid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.collectiveEventType.rescheduledBookingUid = responseBody.data.uid;
});
});
it("should not send an email when rescheduling round robin booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 12, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.roundRobinEventType.createdBookingUid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.roundRobinEventType.rescheduledBookingUid = responseBody.data.uid;
emailsDisabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
});
});
});
describe("reassign", () => {
it("should not send an email when manually reassigning round robin booking", async () => {
const reassignToId =
emailsDisabledSetup.roundRobinEventType.currentHostId === emailsDisabledSetup.member1.id
? emailsDisabledSetup.member2.id
: emailsDisabledSetup.member1.id;
return request(app.getHttpServer())
.post(
`/v2/bookings/${emailsDisabledSetup.roundRobinEventType.rescheduledBookingUid}/reassign/${reassignToId}`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeUpdatedEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.roundRobinEventType.currentHostId = reassignToId;
});
});
it("should not send an email when automatically reassigning round robin booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.roundRobinEventType.rescheduledBookingUid}/reassign`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerReassignedEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeUpdatedEmail.prototype.getHtml).not.toHaveBeenCalled();
});
});
});
describe("cancel", () => {
it("should not send an email when cancelling a collective booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.collectiveEventType.rescheduledBookingUid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
});
});
});
it("should not send an email when cancelling a round robin booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.roundRobinEventType.rescheduledBookingUid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
});
});
});
describe("OAuth client team bookings - emails enabled", () => {
beforeEach(async () => {
jest.clearAllMocks();
});
describe("book", () => {
it("should send an email when booking collective event", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.collectiveEventType.id,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
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);
if (responseDataIsBooking(responseBody.data)) {
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
emailsEnabledSetup.collectiveEventType.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should send an email when booking round robin event", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.roundRobinEventType.id,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
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);
if (responseDataIsBooking(responseBody.data)) {
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
emailsEnabledSetup.roundRobinEventType.createdBookingUid = responseBody.data.uid;
emailsEnabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
describe("reschedule", () => {
it("should send an email when rescheduling collective booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 11, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.collectiveEventType.createdBookingUid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
emailsEnabledSetup.collectiveEventType.rescheduledBookingUid = responseBody.data.uid;
});
});
it("should send an email when rescheduling round robin booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 12, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.roundRobinEventType.createdBookingUid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid = responseBody.data.uid;
emailsEnabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
});
});
});
describe("reassign", () => {
it("should send an email when manually reassigning round robin booking", async () => {
const reassignToId =
emailsEnabledSetup.roundRobinEventType.currentHostId === emailsEnabledSetup.member1.id
? emailsEnabledSetup.member2.id
: emailsEnabledSetup.member1.id;
return request(app.getHttpServer())
.post(
`/v2/bookings/${emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid}/reassign/${reassignToId}`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(AttendeeUpdatedEmail.prototype.getHtml).toHaveBeenCalled();
emailsDisabledSetup.roundRobinEventType.currentHostId = reassignToId;
});
});
it("should send an email when automatically reassigning round robin booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid}/reassign`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerReassignedEmail.prototype.getHtml).toHaveBeenCalled();
expect(AttendeeUpdatedEmail.prototype.getHtml).toHaveBeenCalled();
});
});
});
describe("cancel", () => {
it("should send an email when cancelling a collective booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.collectiveEventType.rescheduledBookingUid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
});
});
it("should send an email when cancelling round robin booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
});
});
});
});
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.member1.email);
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.member1.email);
await bookingsRepositoryFixture.deleteAllBookings(
emailsEnabledSetup.member1.id,
emailsEnabledSetup.member1.email
);
await bookingsRepositoryFixture.deleteAllBookings(
emailsDisabledSetup.member1.id,
emailsDisabledSetup.member1.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;
}
});
@@ -0,0 +1,482 @@
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 { 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";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.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 { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
OrganizerScheduledEmail,
AttendeeScheduledEmail,
OrganizerRescheduledEmail,
AttendeeRescheduledEmail,
OrganizerCancelledEmail,
AttendeeCancelledEmail,
} from "@calcom/platform-libraries";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { CancelBookingInput_2024_08_13 } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
jest
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
type EmailSetup = {
user: User;
eventTypeId: number;
recurringEventTypeId: number;
createdBookingUid: string;
rescheduledBookingUid: string;
};
describe("Bookings Endpoints 2024-08-13 user emails", () => {
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 teamRepositoryFixture: TeamRepositoryFixture;
let emailsEnabledSetup: EmailSetup;
let emailsDisabledSetup: EmailSetup;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.overrideGuard(ApiAuthGuard)
.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" });
await setupEnabledEmails();
await setupDisabledEmails();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function setupEnabledEmails() {
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
const user = await userRepositoryFixture.create({
email: `alice-${Math.floor(Math.random() * 1000)}@gmail.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
user.id
);
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: 3, interval: 1 },
},
user.id
);
emailsEnabledSetup = {
user,
eventTypeId: event.id,
recurringEventTypeId: recurringEvent.id,
createdBookingUid: "",
rescheduledBookingUid: "",
};
}
async function setupDisabledEmails() {
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
const user = await userRepositoryFixture.create({
email: `bob-${Math.floor(Math.random() * 1000)}@gmail.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
user.id
);
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: 3, interval: 1 },
},
user.id
);
emailsDisabledSetup = {
user,
eventTypeId: event.id,
recurringEventTypeId: recurringEvent.id,
createdBookingUid: "",
rescheduledBookingUid: "",
};
}
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
areEmailsEnabled: emailsEnabled,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
beforeEach(async () => {
jest.clearAllMocks();
});
describe("OAuth client managed user bookings - emails disabled", () => {
it("should not send an email when creating a booking", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: emailsDisabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
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);
if (responseDataIsBooking(responseBody.data)) {
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should not send an email when creating a recurring booking", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
eventTypeId: emailsDisabledSetup.recurringEventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
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);
if (responseDataIsRecurringBooking(responseBody.data)) {
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
} else {
throw new Error("Invalid response data - expected booking array but received single booking");
}
});
});
it("should not send an email when rescheduling a booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.createdBookingUid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
it("should not send an email when cancelling a booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.rescheduledBookingUid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
});
});
});
describe("OAuth client managed user bookings - emails enabled", () => {
it("should send an email when creating a booking", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
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);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should send an email when creating a recurring booking", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.recurringEventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
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);
if (responseDataIsRecurringBooking(responseBody.data)) {
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
} else {
throw new Error("Invalid response data - expected booking array but received single booking");
}
});
});
it("should send an email when rescheduling a booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
it("should send an email when cancelling a booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.rescheduledBookingUid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
});
});
});
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.user.email);
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.user.email);
await bookingsRepositoryFixture.deleteAllBookings(
emailsEnabledSetup.user.id,
emailsEnabledSetup.user.email
);
await bookingsRepositoryFixture.deleteAllBookings(
emailsDisabledSetup.user.id,
emailsDisabledSetup.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);
}
});
@@ -1,6 +1,5 @@
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 { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.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";
@@ -25,26 +24,9 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
AttendeeScheduledEmail,
OrganizerScheduledEmail,
OrganizerReassignedEmail,
} from "@calcom/platform-libraries";
import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
jest.spyOn(AttendeeScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
jest.spyOn(OrganizerScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
jest.spyOn(OrganizerReassignedEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
describe("Bookings Endpoints 2024-08-13", () => {
describe("Reassign bookings", () => {
let app: INestApplication;
@@ -97,14 +79,19 @@ describe("Bookings Endpoints 2024-08-13", () => {
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" });
oAuthClient = await createOAuthClient(organization.id);
team = await teamRepositoryFixture.create({
name: "team 1",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
connect: {
id: oAuthClient.id,
},
},
});
oAuthClient = await createOAuthClient(organization.id);
teamUser1 = await userRepositoryFixture.create({
email: teamUserEmail,
locale: "it",
@@ -72,6 +72,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -375,6 +380,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -577,6 +587,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -22,7 +22,6 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import { AttendeeCancelledEmail, OrganizerCancelledEmail } from "@calcom/platform-libraries";
import {
CancelSeatedBookingInput_2024_08_13,
CreateRecurringSeatedBookingOutput_2024_08_13,
@@ -39,14 +38,6 @@ import {
} from "@calcom/platform-types";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
jest.spyOn(AttendeeCancelledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
jest.spyOn(OrganizerCancelledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
describe("Bookings Endpoints 2024-08-13", () => {
describe("Seated bookings", () => {
let app: INestApplication;
@@ -102,6 +93,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -721,6 +717,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -85,30 +85,50 @@ describe("Bookings Endpoints 2024-08-13", () => {
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" });
oAuthClient = await createOAuthClient(organization.id);
team1 = await teamRepositoryFixture.create({
name: "team 1",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
connect: {
id: oAuthClient.id,
},
},
});
team2 = await teamRepositoryFixture.create({
name: "team 2",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
connect: {
id: oAuthClient.id,
},
},
});
oAuthClient = await createOAuthClient(organization.id);
teamUser = await userRepositoryFixture.create({
email: teamUserEmail,
locale: "it",
name: "orgUser1team1",
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
teamUser2 = await userRepositoryFixture.create({
email: teamUserEmail2,
locale: "es",
name: "orgUser2team1",
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -96,6 +96,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
username: `bob-${Math.floor(Math.random() * 1000)}@gmail.com`,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -328,12 +334,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
},
};
const beforeCreate = new Date();
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const afterCreate = new Date();
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
@@ -344,6 +352,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString());
@@ -367,6 +376,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
email: body.attendee.email,
...body.bookingFieldsResponses,
});
// Check createdAt date is between the time of the request and after the request
const createdAtDate = new Date(data.createdAt);
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
expect(data.metadata).toEqual(body.metadata);
createdBooking = data;
} else {
@@ -495,6 +510,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.id).toEqual(createdBooking.id);
expect(data.uid).toEqual(createdBooking.uid);
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.status).toEqual(createdBooking.status);
expect(data.start).toEqual(createdBooking.start);
expect(data.end).toEqual(createdBooking.end);
@@ -503,6 +519,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
expect(data.location).toEqual(createdBooking.location);
expect(data.absentHost).toEqual(createdBooking.absentHost);
expect(data.createdAt).toEqual(createdBooking.createdAt);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
@@ -976,12 +993,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
reschedulingReason: "Flying to mars that day",
};
const beforeCreate = new Date();
return request(app.getHttpServer())
.post(`/v2/bookings/${createdBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const afterCreate = new Date();
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
@@ -1003,6 +1022,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.absentHost).toEqual(createdBooking.absentHost);
expect(data.metadata).toEqual(createdBooking.metadata);
// When a booking is rescheduled, a new booking is created and the old booking is cancelled.
// We want to make sure the createdAt date of the new booking is between the beforeCreate and afterCreate dates.
const createdAtDate = new Date(data.createdAt);
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
rescheduledBooking = data;
});
});
@@ -1,9 +1,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 { 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";
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";
@@ -71,6 +68,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -10,7 +10,12 @@ import {
RecurringBookingOutput_2024_08_13,
} from "@calcom/platform-types";
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
@ApiExtraModels(
BookingOutput_2024_08_13,
RecurringBookingOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
GetRecurringSeatedBookingOutput_2024_08_13
)
export class CancelBookingOutput_2024_08_13 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
@@ -10,7 +10,12 @@ import {
CreateRecurringSeatedBookingOutput_2024_08_13,
} from "@calcom/platform-types";
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
@ApiExtraModels(
BookingOutput_2024_08_13,
RecurringBookingOutput_2024_08_13,
CreateSeatedBookingOutput_2024_08_13,
CreateRecurringSeatedBookingOutput_2024_08_13
)
export class CreateBookingOutput_2024_08_13 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
@@ -21,6 +26,8 @@ export class CreateBookingOutput_2024_08_13 {
oneOf: [
{ $ref: getSchemaPath(BookingOutput_2024_08_13) },
{ type: "array", items: { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) } },
{ $ref: getSchemaPath(CreateSeatedBookingOutput_2024_08_13) },
{ type: "array", items: { $ref: getSchemaPath(CreateRecurringSeatedBookingOutput_2024_08_13) } },
],
description:
"Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects",
@@ -12,7 +12,12 @@ import {
RecurringBookingOutput_2024_08_13,
} from "@calcom/platform-types";
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
@ApiExtraModels(
BookingOutput_2024_08_13,
RecurringBookingOutput_2024_08_13,
CreateSeatedBookingOutput_2024_08_13,
CreateRecurringSeatedBookingOutput_2024_08_13
)
export class RescheduleBookingOutput_2024_08_13 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
@@ -10,6 +10,7 @@ import { UsersRepository, UserWithProfile } from "@/modules/users/users.reposito
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
import { BadRequestException } from "@nestjs/common";
import { Request } from "express";
import { z } from "zod";
import {
handleNewBooking,
@@ -38,6 +39,7 @@ import {
CancelBookingInput,
} from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
import { EventType } from "@calcom/prisma/client";
type CreatedBooking = {
hosts: { id: number }[];
@@ -45,6 +47,14 @@ type CreatedBooking = {
start: string;
};
const eventTypeBookingFieldSchema = z.object({
name: z.string(),
required: z.boolean(),
editable: z.string(),
});
const eventTypeBookingFieldsSchema = z.array(eventTypeBookingFieldSchema);
@Injectable()
export class BookingsService_2024_08_13 {
private readonly logger = new Logger("BookingsService");
@@ -70,6 +80,8 @@ export class BookingsService_2024_08_13 {
const isRecurring = !!eventType?.recurringEvent;
const isSeated = !!eventType?.seatsPerTimeSlot;
await this.hasRequiredBookingFieldsResponses(body, eventType);
if (isRecurring && isSeated) {
return await this.createRecurringSeatedBooking(request, body);
}
@@ -91,6 +103,28 @@ export class BookingsService_2024_08_13 {
}
}
async hasRequiredBookingFieldsResponses(body: CreateBookingInput, eventType: EventType | null) {
const bookingFields = body.bookingFieldsResponses;
if (!bookingFields || !eventType || !eventType.bookingFields) {
return true;
}
// note(Lauris): we filter out system fields, because some of them are set by default and name and email are passed in the body.attendee
const eventTypeBookingFields = eventTypeBookingFieldsSchema
.parse(eventType.bookingFields)
.filter((field) => !field.editable.startsWith("system"));
for (const field of eventTypeBookingFields) {
if (field.required && !(field.name in bookingFields)) {
throw new BadRequestException(`
Missing required booking field response: ${field.name} - it is required by the event type booking fields, but missing in the bookingFieldsResponses.
You can fetch the event type with ID ${eventType.id} to see the required fields.`);
}
}
return true;
}
async createInstantBooking(request: Request, body: CreateInstantBookingInput_2024_08_13) {
const bookingRequest = await this.inputService.createBookingRequest(request, body);
const booking = await handleInstantMeeting(bookingRequest);
@@ -329,12 +363,17 @@ export class BookingsService_2024_08_13 {
async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_2024_08_13) {
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
const platformClientParams = bookingBefore?.eventTypeId
? await this.inputService.getOAuthClientParams(bookingBefore.eventTypeId)
: undefined;
await handleMarkNoShow({
bookingUid,
attendees: bodyTransformed.attendees,
noShowHost: bodyTransformed.noShowHost,
userId: bookingOwnerId,
platformClientParams,
});
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
@@ -389,11 +428,19 @@ export class BookingsService_2024_08_13 {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
const profile = this.usersService.getUserMainProfile(requestUser);
await roundRobinReassignment({
bookingId: booking.id,
orgId: profile?.organizationId || null,
emailsEnabled,
platformClientParams,
});
const reassigned = await this.bookingsRepository.getByUidWithUser(bookingUid);
@@ -420,6 +467,12 @@ export class BookingsService_2024_08_13 {
throw new NotFoundException(`User with id=${newUserId} was not found in the database`);
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
const profile = this.usersService.getUserMainProfile(user);
const reassigned = await roundRobinManualReassignment({
@@ -428,6 +481,8 @@ export class BookingsService_2024_08_13 {
orgId: profile?.organizationId || null,
reassignReason: body.reason,
reassignedById,
emailsEnabled,
platformClientParams,
});
return this.outputService.getOutputReassignedBooking(reassigned);
@@ -439,6 +494,12 @@ export class BookingsService_2024_08_13 {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
await confirmBookingHandler({
ctx: {
user: requestUser,
@@ -447,6 +508,8 @@ export class BookingsService_2024_08_13 {
bookingId: booking.id,
confirmed: true,
recurringEventId: booking.recurringEventId,
emailsEnabled,
platformClientParams,
},
});
@@ -459,6 +522,12 @@ export class BookingsService_2024_08_13 {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
await confirmBookingHandler({
ctx: {
user: requestUser,
@@ -468,6 +537,8 @@ export class BookingsService_2024_08_13 {
confirmed: false,
recurringEventId: booking.recurringEventId,
reason,
emailsEnabled,
platformClientParams,
},
});
@@ -18,7 +18,6 @@ import { NextApiRequest } from "next/types";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries";
import {
CancelBookingInput,
@@ -33,19 +32,10 @@ import {
RescheduleBookingInput_2024_08_13,
RescheduleSeatedBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { EventType } from "@calcom/prisma/client";
import { EventType, PlatformOAuthClient } from "@calcom/prisma/client";
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
const DEFAULT_PLATFORM_PARAMS = {
platformClientId: "",
platformCancelUrl: "",
platformRescheduleUrl: "",
platformBookingUrl: "",
arePlatformEmailsEnabled: false,
platformBookingLocation: undefined,
};
type OAuthRequestParams = {
platformClientId: string;
platformRescheduleUrl: string;
@@ -92,30 +82,53 @@ export class InputBookingsService_2024_08_13 {
body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13
): Promise<BookingRequest> {
const bodyTransformed = await this.transformInputCreateBooking(body);
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
const requestId = request.get("X-Request-Id");
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const oAuthParams = oAuthClientId
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
: DEFAULT_PLATFORM_PARAMS;
const location = request.body.location || request.body.meetingUrl;
this.logger.log(`createBookingRequest_2024_04_15`, {
requestId,
requestId: request.get("X-Request-Id"),
ownerId: userId,
location,
oAuthClientId,
...oAuthParams,
oAuthClientParams,
});
Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled };
if (oAuthClientParams) {
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled };
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: false };
}
return newRequest as unknown as BookingRequest;
}
async getOAuthClientParams(eventTypeId: number) {
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
let oAuthClient: PlatformOAuthClient | null = null;
if (eventType?.userId) {
oAuthClient = await this.oAuthClientRepository.getByUserId(eventType.userId);
} else if (eventType?.teamId) {
oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId);
}
if (oAuthClient) {
return {
platformClientId: oAuthClient.id,
platformCancelUrl: oAuthClient.bookingCancelRedirectUri,
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
platformBookingUrl: oAuthClient.bookingRedirectUri,
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
};
}
return undefined;
}
async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) {
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
inputBooking.eventTypeId
@@ -178,21 +191,23 @@ export class InputBookingsService_2024_08_13 {
): Promise<BookingRequest> {
// note(Lauris): update to this.transformInputCreate when rescheduling is implemented
const bodyTransformed = await this.transformInputCreateRecurringBooking(body);
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const oAuthParams = oAuthClientId
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
: DEFAULT_PLATFORM_PARAMS;
const location = request.body.location || request.body.meetingUrl;
Object.assign(newRequest, {
userId,
...oAuthParams,
platformBookingLocation: location,
noEmail: !oAuthParams.arePlatformEmailsEnabled,
});
if (oAuthClientParams) {
Object.assign(newRequest, {
userId,
...oAuthClientParams,
platformBookingLocation: location,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
});
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
}
newRequest.body = bodyTransformed.map((event) => ({
...event,
@@ -282,18 +297,19 @@ export class InputBookingsService_2024_08_13 {
? await this.transformInputRescheduleSeatedBooking(bookingUid, body)
: await this.transformInputRescheduleBooking(bookingUid, body);
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
const oAuthClientParams = await this.getOAuthClientParams(bodyTransformed.eventTypeId);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const oAuthParams = oAuthClientId
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
: DEFAULT_PLATFORM_PARAMS;
const location = await this.getRescheduleBookingLocation(bookingUid);
Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled };
if (oAuthClientParams) {
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled };
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: false };
}
return newRequest as unknown as BookingRequest;
}
@@ -416,24 +432,6 @@ export class InputBookingsService_2024_08_13 {
}
}
private async createBookingRequestOAuthClientParams(clientId: string) {
const params = DEFAULT_PLATFORM_PARAMS;
try {
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
if (client) {
params.platformClientId = clientId;
params.platformCancelUrl = client.bookingCancelRedirectUri ?? "";
params.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
params.platformBookingUrl = client.bookingRedirectUri ?? "";
params.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false;
}
return params;
} catch (err) {
this.logger.error(err);
return params;
}
}
transformGetBookingsFilters(queryParams: GetBookingsInput_2024_08_13) {
return {
attendeeEmail: queryParams.attendeeEmail,
@@ -466,17 +464,26 @@ export class InputBookingsService_2024_08_13 {
const bodyTransformed = this.isCancelSeatedBody(body)
? await this.transformInputCancelSeatedBooking(bookingUid, body)
: await this.transformInputCancelBooking(bookingUid, body);
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
const booking = await this.bookingsRepository.getByUid(bodyTransformed.uid);
if (!booking) {
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
}
const oAuthClientParams = booking.eventTypeId
? await this.getOAuthClientParams(booking.eventTypeId)
: undefined;
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const oAuthParams = oAuthClientId
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
: DEFAULT_PLATFORM_PARAMS;
Object.assign(newRequest, { userId, ...oAuthParams });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled };
if (oAuthClientParams) {
Object.assign(newRequest, { userId, ...oAuthClientParams });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled };
} else {
Object.assign(newRequest, { userId });
newRequest.body = { ...bodyTransformed, noEmail: false };
}
return newRequest as unknown as BookingRequest;
}
@@ -20,7 +20,13 @@ import { Booking, BookingSeat } from "@calcom/prisma/client";
export const bookingResponsesSchema = z
.object({
email: z.string(),
name: z.string(),
name: z.union([
z.string(),
z.object({
firstName: z.string(),
lastName: z.string(),
}),
]),
guests: z.array(z.string()).optional(),
rescheduleReason: z.string().optional(),
})
@@ -31,12 +37,20 @@ export const seatedBookingResponsesSchema = z
responses: z
.object({
email: z.string(),
name: z.string(),
name: z.union([
z.string(),
z.object({
firstName: z.string(),
lastName: z.string(),
}),
]),
})
.passthrough(),
})
.passthrough();
type DatabaseUser = { id: number; name: string | null; email: string; username: string | null };
type DatabaseBooking = Booking & {
eventType: {
id: number;
@@ -50,16 +64,13 @@ type DatabaseBooking = Booking & {
noShow: boolean | null;
bookingSeat?: BookingSeat | null;
}[];
} & { user: { id: number; name: string | null; email: string } | null };
user: DatabaseUser | null;
createdAt: Date;
};
type BookingWithUser = Booking & { user: { id: number; name: string | null; email: string } | null };
type BookingWithUser = Booking & { user: DatabaseUser | null };
const bookingUserMetadataSchema = bookingMetadataSchema.transform((data) => {
if (data === null) return {};
// note(Lauris): return only user defined metadata
const { videoCallUrl, ...rest } = data;
return rest;
});
type DatabaseMetadata = z.infer<typeof bookingMetadataSchema>;
const seatedBookingMetadataSchema = z.object({}).catchall(z.string());
@@ -71,16 +82,16 @@ export class OutputBookingsService_2024_08_13 {
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
const duration = dateEnd.diff(dateStart, "minutes").minutes;
const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses);
const metadata = bookingUserMetadataSchema.parse(databaseBooking.metadata);
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const booking = {
id: databaseBooking.id,
uid: databaseBooking.uid,
title: databaseBooking.title,
description: databaseBooking.description,
hosts: [databaseBooking.user],
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason: databaseBooking.cancellationReason || undefined,
reschedulingReason: bookingResponses?.rescheduledReason,
@@ -99,19 +110,44 @@ export class OutputBookingsService_2024_08_13 {
absent: !!attendee.noShow,
})),
guests: bookingResponses.guests,
location: databaseBooking.location,
location,
// note(Lauris): meetingUrl is deprecated
meetingUrl: databaseBooking.location,
meetingUrl: location,
absentHost: !!databaseBooking.noShowHost,
createdAt: databaseBooking.createdAt,
};
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
// note(Lauris): I don't know why plainToClass erases bookings responses and metadata so attaching manually
bookingTransformed.bookingFieldsResponses = bookingResponses;
bookingTransformed.metadata = metadata;
bookingTransformed.metadata = this.getUserDefinedMetadata(metadata);
return bookingTransformed;
}
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
if (databaseMetadata === null) return {};
const { videoCallUrl, ...userDefinedMetadata } = databaseMetadata;
return userDefinedMetadata;
}
getHost(user: DatabaseUser | null) {
if (!user) {
return {
id: "unknown",
name: "unknown",
email: "unknown",
username: "unknown",
};
}
return {
...user,
username: user.username || "unknown",
};
}
async getOutputRecurringBookings(bookingsIds: number[]) {
const transformed = [];
@@ -131,16 +167,16 @@ export class OutputBookingsService_2024_08_13 {
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
const duration = dateEnd.diff(dateStart, "minutes").minutes;
const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses);
const metadata = bookingUserMetadataSchema.parse(databaseBooking.metadata);
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const booking = {
id: databaseBooking.id,
uid: databaseBooking.uid,
title: databaseBooking.title,
description: databaseBooking.description,
hosts: [databaseBooking.user],
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason: databaseBooking.cancellationReason || undefined,
reschedulingReason: bookingResponses?.rescheduledReason,
@@ -159,12 +195,13 @@ export class OutputBookingsService_2024_08_13 {
absent: !!attendee.noShow,
})),
guests: bookingResponses.guests,
location: databaseBooking.location,
location,
// note(Lauris): meetingUrl is deprecated
meetingUrl: databaseBooking.location,
meetingUrl: location,
recurringBookingUid: databaseBooking.recurringEventId,
absentHost: !!databaseBooking.noShowHost,
bookingFieldsResponses: databaseBooking.responses,
createdAt: databaseBooking.createdAt,
};
const bookingTransformed = plainToClass(RecurringBookingOutput_2024_08_13, booking, {
@@ -172,7 +209,7 @@ export class OutputBookingsService_2024_08_13 {
});
// note(Lauris): I don't know why plainToClass erases bookings responses and metadata so attaching manually
bookingTransformed.bookingFieldsResponses = bookingResponses;
bookingTransformed.metadata = metadata;
bookingTransformed.metadata = this.getUserDefinedMetadata(metadata);
return bookingTransformed;
}
@@ -188,13 +225,15 @@ export class OutputBookingsService_2024_08_13 {
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
const duration = dateEnd.diff(dateStart, "minutes").minutes;
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const booking = {
id: databaseBooking.id,
uid: databaseBooking.uid,
title: databaseBooking.title,
description: databaseBooking.description,
hosts: [databaseBooking.user],
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
@@ -204,10 +243,11 @@ export class OutputBookingsService_2024_08_13 {
// note(Lauris): eventTypeId is deprecated
eventTypeId: databaseBooking.eventTypeId,
attendees: [],
location: databaseBooking.location,
location,
// note(Lauris): meetingUrl is deprecated
meetingUrl: databaseBooking.location,
meetingUrl: location,
absentHost: !!databaseBooking.noShowHost,
createdAt: databaseBooking.createdAt,
};
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
@@ -280,13 +320,15 @@ export class OutputBookingsService_2024_08_13 {
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
const duration = dateEnd.diff(dateStart, "minutes").minutes;
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const booking = {
id: databaseBooking.id,
uid: databaseBooking.uid,
title: databaseBooking.title,
description: databaseBooking.description,
hosts: [databaseBooking.user],
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason: databaseBooking.cancellationReason || undefined,
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
@@ -297,11 +339,12 @@ export class OutputBookingsService_2024_08_13 {
// note(Lauris): eventTypeId is deprecated
eventTypeId: databaseBooking.eventTypeId,
attendees: [],
location: databaseBooking.location,
location,
// note(Lauris): meetingUrl is deprecated
meetingUrl: databaseBooking.location,
meetingUrl: location,
recurringBookingUid: databaseBooking.recurringEventId,
absentHost: !!databaseBooking.noShowHost,
createdAt: databaseBooking.createdAt,
};
const parsed = plainToClass(GetRecurringSeatedBookingOutput_2024_08_13, booking, {
+3 -17
View File
@@ -1,6 +1,6 @@
import { WinstonTransport as AxiomTransport } from "@axiomhq/winston";
import type { LoggerOptions } from "winston";
import { format, transports as Transports } from "winston";
import { format, transports as Transports, config } from "winston";
import type Transport from "winston-transport";
const formattedTimestamp = format.timestamp({
@@ -8,14 +8,7 @@ const formattedTimestamp = format.timestamp({
});
const colorizer = format.colorize({
colors: {
fatal: "red",
error: "red",
warn: "yellow",
info: "blue",
debug: "white",
trace: "grey",
},
colors: config.npm.colors,
});
const WINSTON_DEV_FORMAT = format.combine(
@@ -26,14 +19,7 @@ const WINSTON_DEV_FORMAT = format.combine(
);
const WINSTON_PROD_FORMAT = format.combine(format.errors({ stack: true }), formattedTimestamp, format.json());
export const logLevels = {
fatal: 0,
error: 1,
warn: 2,
info: 3,
debug: 4,
trace: 5,
} as const;
export const logLevels = config.npm.levels;
export const loggerConfig = (): LoggerOptions => {
const isProduction = process.env.NODE_ENV === "production";
+8 -8
View File
@@ -56,7 +56,7 @@ 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(
this.logger.verbose(
`Tracker "${tracker}" generated based on: Bearer token "${request.get(
"Authorization"
)}", OAuth client ID "${request.get(X_CAL_CLIENT_ID)}" and IP "${request.ip}"`
@@ -89,7 +89,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
private async handleNonApiKeyRequest(tracker: string, response: Response): Promise<boolean> {
const rateLimit = this.getDefaultRateLimit(tracker);
this.logger.log(`Tracker "${tracker}" uses default rate limits because it is not tracking api key:
this.logger.verbose(`Tracker "${tracker}" uses default rate limits because it is not tracking api key:
${JSON.stringify(rateLimit, null, 2)}
`);
@@ -135,7 +135,7 @@ 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:
this.logger.verbose(`Tracker "${tracker}" rate limits retrieved from redis cache:
${cachedRateLimits}
`);
return rateLimitsSchema.parse(JSON.parse(cachedRateLimits));
@@ -158,13 +158,13 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
});
if (rateLimits) {
this.logger.log(`Tracker "${tracker}" rate limits retrieved from database:
this.logger.verbose(`Tracker "${tracker}" rate limits retrieved from database:
${JSON.stringify(rateLimits, null, 2)}`);
}
if (!rateLimits || rateLimits.length === 0) {
rateLimits = [this.getDefaultRateLimit(tracker)];
this.logger.log(`Tracker "${tracker}" rate limits not found in database. Using default rate limits:
this.logger.verbose(`Tracker "${tracker}" rate limits not found in database. Using default rate limits:
${JSON.stringify(rateLimits, null, 2)}`);
}
@@ -194,10 +194,10 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
);
response.setHeader(`X-RateLimit-Reset-${nameFirstUpper}`, timeToBlockExpire || timeToExpire);
this.logger.log(
this.logger.verbose(
`Tracker "${tracker}" rate limit "${name}" incremented. isBlocked ${isBlocked}, totalHits ${totalHits}, timeToExpire ${timeToExpire}, timeToBlockExpire ${timeToBlockExpire}`
);
this.logger.log(
this.logger.verbose(
`Tracker "${tracker}" rate limit "${name}" response headers:
X-RateLimit-Limit-${nameFirstUpper}: ${limit},
X-RateLimit-Remaining-${nameFirstUpper}: ${timeToBlockExpire ? 0 : Math.max(0, limit - totalHits)},
@@ -227,7 +227,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
return `ip_${request.ip}`;
}
this.logger.log(`no tracker found: ${request.url}`);
this.logger.verbose(`no tracker found: ${request.url}`);
return "unknown";
}
}
@@ -49,7 +49,7 @@ export class PlatformPlanGuard implements CanActivate {
canAccess = hasMinimumPlan({
currentPlan: team.platformBilling?.plan as PlatformPlanType,
minimumPlan: minimumPlan,
plans: ["STARTER", "ESSENTIALS", "SCALE", "ENTERPRISE"],
plans: ["FREE", "STARTER", "ESSENTIALS", "SCALE", "ENTERPRISE"],
});
}
}
@@ -16,6 +16,7 @@ import { Test } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User, EventType } from "@prisma/client";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
@@ -79,6 +80,7 @@ describe("OAuth Client Users Endpoints", () => {
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let postResponseData: CreateManagedUserOutput["data"];
@@ -103,26 +105,33 @@ describe("OAuth Client Users Endpoints", () => {
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
organization = await teamRepositoryFixture.create({ name: "organization" });
organization = await teamRepositoryFixture.create({
name: "organization",
isPlatform: true,
isOrganization: true,
});
oAuthClient = await createOAuthClient(organization.id);
await profilesRepositoryFixture.create({
uid: "asd-asd",
uid: "asd1qwwqeqw-asddsadasd",
username: platformAdminEmail,
organization: { connect: { id: organization.id } },
movedFromUser: {
connect: {
id: platformAdmin.id,
},
},
user: {
connect: { id: platformAdmin.id },
},
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: platformAdmin.id } },
team: { connect: { id: organization.id } },
accepted: true,
});
await app.init();
});
@@ -370,7 +379,7 @@ describe("OAuth Client Users Endpoints", () => {
let teamRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let postResponseData: CreateManagedUserOutput["data"];
const userEmail = "oauth-client-users-user@gmail.com";
@@ -390,10 +399,11 @@ describe("OAuth Client Users Endpoints", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({
name: "Testy Organization",
isOrganization: true,
isPlatform: true,
});
owner = await userRepositoryFixture.create({
@@ -417,6 +427,13 @@ describe("OAuth Client Users Endpoints", () => {
},
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: owner.id } },
team: { connect: { id: organization.id } },
accepted: true,
});
oAuthClient1 = await createOAuthClient(organization.id);
oAuthClient2 = await createOAuthClient(organization.id);
@@ -1,6 +1,8 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { Locales } from "@/lib/enums/locales";
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
import { CreateManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
import { GetManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-user.output";
import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output";
@@ -29,7 +31,7 @@ import {
NotFoundException,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { User, MembershipRole } from "@prisma/client";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { Pagination } from "@calcom/platform-types";
@@ -38,7 +40,7 @@ import { Pagination } from "@calcom/platform-types";
path: "/v2/oauth-clients/:clientId/users",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, OAuthClientGuard)
@UseGuards(ApiAuthGuard, OAuthClientGuard, OrganizationRolesGuard)
@DocsTags("Platform / Managed Users")
export class OAuthClientUsersController {
private readonly logger = new Logger("UserController");
@@ -52,6 +54,7 @@ export class OAuthClientUsersController {
@Get("/")
@ApiOperation({ summary: "Get all managed users" })
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
async getManagedUsers(
@Param("clientId") oAuthClientId: string,
@Query() queryParams: Pagination
@@ -73,6 +76,7 @@ export class OAuthClientUsersController {
@Post("/")
@ApiOperation({ summary: "Create a managed user" })
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
async createUser(
@Param("clientId") oAuthClientId: string,
@Body() body: CreateManagedUserInput
@@ -104,6 +108,7 @@ export class OAuthClientUsersController {
@Get("/:userId")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Get a managed user" })
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
async getUserById(
@Param("clientId") clientId: string,
@Param("userId") userId: number
@@ -119,6 +124,7 @@ export class OAuthClientUsersController {
@Patch("/:userId")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Update a managed user" })
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
async updateUser(
@Param("clientId") clientId: string,
@Param("userId") userId: number,
@@ -138,6 +144,7 @@ export class OAuthClientUsersController {
@Delete("/:userId")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Delete a managed user" })
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
async deleteUser(
@Param("clientId") clientId: string,
@Param("userId") userId: number
@@ -155,7 +162,12 @@ export class OAuthClientUsersController {
@Post("/:userId/force-refresh")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Force refresh tokens" })
@ApiOperation({
summary: "Force refresh tokens",
description: `If you have lost managed user access or refresh token, then you can get new ones by using OAuth credentials.
Each access token is valid for 60 minutes and each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have \`calAccessToken\` and \`calRefreshToken\` columns.`,
})
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
async forceRefresh(
@Param("userId") userId: number,
@Param("clientId") oAuthClientId: string
@@ -24,10 +24,10 @@ import { withNextAuth } from "test/utils/withNextAuth";
import { PlatformOAuthClient, Team, Webhook } from "@calcom/prisma/client";
describe("EventTypes WebhooksController (e2e)", () => {
describe("OAuth client WebhooksController (e2e)", () => {
let app: INestApplication;
const userEmail = "event-types-webhook-controller-e2e@api.com";
const otherUserEmail = "other-event-types-webhook-controller-e2e@api.com";
const userEmail = "oauth-client-webhook-controller-e2e@api.com";
const otherUserEmail = "other-oauth-client-webhook-controller-e2e@api.com";
let user: UserWithProfile;
let otherUser: UserWithProfile;
let oAuthClient: PlatformOAuthClient;
@@ -30,6 +30,7 @@ import {
ApiExcludeEndpoint as DocsExcludeEndpoint,
ApiBadRequestResponse as DocsBadRequestResponse,
ApiHeader as DocsHeader,
ApiOperation,
} from "@nestjs/swagger";
import { Response as ExpressResponse } from "express";
@@ -39,7 +40,6 @@ import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
path: "/v2/oauth/:clientId",
version: API_VERSIONS_VALUES,
})
@DocsTags("OAuth")
export class OAuthFlowController {
constructor(
private readonly oauthClientRepository: OAuthClientRepository,
@@ -115,13 +115,18 @@ export class OAuthFlowController {
@Post("/refresh")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@DocsTags("Managed users")
@DocsTags("Platform / Managed Users")
@DocsHeader({
name: X_CAL_SECRET_KEY,
description: "OAuth client secret key.",
required: true,
})
async refreshAccessToken(
@ApiOperation({
summary: "Refresh managed user tokens",
description: `If managed user access token is expired then get a new one using this endpoint. Each access token is valid for 60 minutes and
each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have \`calAccessToken\` and \`calRefreshToken\` columns.`,
})
async refreshTokens(
@Param("clientId") clientId: string,
@Headers(X_CAL_SECRET_KEY) secretKey: string,
@Body() body: RefreshTokenInput
@@ -99,4 +99,28 @@ export class OAuthClientRepository {
where: { id: clientId },
});
}
async getByUserId(userId: number) {
return this.dbRead.prisma.platformOAuthClient.findFirst({
where: {
users: {
some: {
id: userId,
},
},
},
});
}
async getByTeamId(teamId: number) {
return this.dbRead.prisma.platformOAuthClient.findFirst({
where: {
teams: {
some: {
id: teamId,
},
},
},
});
}
}
@@ -14,7 +14,7 @@ export class UpdateManagedUserInput {
name?: string;
@IsOptional()
@IsIn(["12", "24"])
@IsIn([12, 24])
@ApiProperty({ example: 12, enum: [12, 24], description: "Must be 12 or 24" })
timeFormat?: TimeFormat;
+674 -54
View File
@@ -362,6 +362,7 @@
"post": {
"operationId": "OAuthClientUsersController_forceRefresh",
"summary": "Force refresh tokens",
"description": "If you have lost managed user access or refresh token, then you can get new ones by using OAuth credentials.\n Each access token is valid for 60 minutes and each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have `calAccessToken` and `calRefreshToken` columns.",
"parameters": [
{
"name": "userId",
@@ -397,6 +398,57 @@
]
}
},
"/v2/oauth/{clientId}/refresh": {
"post": {
"operationId": "OAuthFlowController_refreshTokens",
"summary": "Refresh managed user tokens",
"description": "If managed user access token is expired then get a new one using this endpoint. Each access token is valid for 60 minutes and \n each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have `calAccessToken` and `calRefreshToken` columns.",
"parameters": [
{
"name": "clientId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"required": true,
"in": "header",
"description": "OAuth client secret key.",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RefreshTokenInput"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/KeysResponseDto"
}
}
}
}
},
"tags": [
"Platform / Managed Users"
]
}
},
"/v2/oauth-clients/{clientId}/webhooks": {
"post": {
"operationId": "OAuthClientWebhooksController_createOAuthClientWebhook",
@@ -4439,56 +4491,6 @@
]
}
},
"/v2/oauth/{clientId}/refresh": {
"post": {
"operationId": "OAuthFlowController_refreshAccessToken",
"parameters": [
{
"name": "clientId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"required": true,
"in": "header",
"description": "OAuth client secret key.",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RefreshTokenInput"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/KeysResponseDto"
}
}
}
}
},
"tags": [
"OAuth",
"Managed users"
]
}
},
"/v2/schedules": {
"post": {
"operationId": "SchedulesController_2024_06_11_createSchedule",
@@ -6532,7 +6534,7 @@
"rolling": {
"type": "boolean",
"example": true,
"description": "If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date."
"description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n "
}
},
"required": [
@@ -6560,7 +6562,7 @@
"rolling": {
"type": "boolean",
"example": true,
"description": "If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date."
"description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n "
}
},
"required": [
@@ -13023,6 +13025,33 @@
"attendee"
]
},
"BookingHost": {
"type": "object",
"properties": {
"id": {
"type": "number",
"example": 1
},
"name": {
"type": "string",
"example": "Jane Doe"
},
"username": {
"type": "string",
"example": "jane100"
},
"timeZone": {
"type": "string",
"example": "America/Los_Angeles"
}
},
"required": [
"id",
"name",
"username",
"timeZone"
]
},
"EventType": {
"type": "object",
"properties": {
@@ -13062,7 +13091,7 @@
"hosts": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Host"
"$ref": "#/components/schemas/BookingHost"
}
},
"status": {
@@ -13122,6 +13151,10 @@
"type": "boolean",
"example": true
},
"createdAt": {
"type": "string",
"example": "2024-08-13T15:30:00Z"
},
"metadata": {
"type": "object",
"example": {
@@ -13165,6 +13198,7 @@
"eventTypeId",
"eventType",
"absentHost",
"createdAt",
"attendees"
]
},
@@ -13190,7 +13224,7 @@
"hosts": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Host"
"$ref": "#/components/schemas/BookingHost"
}
},
"status": {
@@ -13250,6 +13284,10 @@
"type": "boolean",
"example": true
},
"createdAt": {
"type": "string",
"example": "2024-08-13T15:30:00Z"
},
"metadata": {
"type": "object",
"example": {
@@ -13297,6 +13335,342 @@
"eventTypeId",
"eventType",
"absentHost",
"createdAt",
"attendees",
"recurringBookingUid"
]
},
"SeatedAttendee": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "John Doe"
},
"timeZone": {
"type": "string",
"example": "America/New_York"
},
"language": {
"type": "string",
"enum": [
"ar",
"ca",
"de",
"es",
"eu",
"he",
"id",
"ja",
"lv",
"pl",
"ro",
"sr",
"th",
"vi",
"az",
"cs",
"el",
"es-419",
"fi",
"hr",
"it",
"km",
"nl",
"pt",
"ru",
"sv",
"tr",
"zh-CN",
"bg",
"da",
"en",
"et",
"fr",
"hu",
"iw",
"ko",
"no",
"pt-BR",
"sk",
"ta",
"uk",
"zh-TW"
],
"example": "en"
},
"absent": {
"type": "boolean",
"example": false
},
"seatUid": {
"type": "string",
"example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1"
},
"bookingFieldsResponses": {
"type": "object",
"description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
"example": {
"customField": "customValue"
}
},
"metadata": {
"type": "object",
"example": {
"key": "value"
}
}
},
"required": [
"name",
"timeZone",
"absent",
"seatUid"
]
},
"CreateSeatedBookingOutput_2024_08_13": {
"type": "object",
"properties": {
"id": {
"type": "number",
"example": 123
},
"uid": {
"type": "string",
"example": "booking_uid_123"
},
"title": {
"type": "string",
"example": "Consultation"
},
"description": {
"type": "string",
"example": "Learn how to integrate scheduling into marketplace."
},
"hosts": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Host"
}
},
"status": {
"type": "string",
"enum": [
"cancelled",
"accepted",
"rejected",
"pending"
],
"example": "accepted"
},
"cancellationReason": {
"type": "string",
"example": "User requested cancellation"
},
"reschedulingReason": {
"type": "string",
"example": "User rescheduled the event"
},
"rescheduledFromUid": {
"type": "string",
"example": "previous_uid_123"
},
"start": {
"type": "string",
"example": "2024-08-13T15:30:00Z"
},
"end": {
"type": "string",
"example": "2024-08-13T16:30:00Z"
},
"duration": {
"type": "number",
"example": 60
},
"eventTypeId": {
"type": "number",
"example": 50,
"deprecated": true,
"description": "Deprecated - rely on 'eventType' object containing the id instead."
},
"eventType": {
"$ref": "#/components/schemas/EventType"
},
"meetingUrl": {
"type": "string",
"description": "Deprecated - rely on 'location' field instead.",
"example": "https://example.com/recurring-meeting",
"deprecated": true
},
"location": {
"type": "string",
"example": "https://example.com/meeting"
},
"absentHost": {
"type": "boolean",
"example": true
},
"createdAt": {
"type": "string",
"example": "2024-08-13T15:30:00Z"
},
"metadata": {
"type": "object",
"example": {
"key": "value"
}
},
"seatUid": {
"type": "string",
"example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1"
},
"attendees": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SeatedAttendee"
}
}
},
"required": [
"id",
"uid",
"title",
"description",
"hosts",
"status",
"start",
"end",
"duration",
"eventTypeId",
"eventType",
"absentHost",
"createdAt",
"seatUid",
"attendees"
]
},
"CreateRecurringSeatedBookingOutput_2024_08_13": {
"type": "object",
"properties": {
"id": {
"type": "number",
"example": 123
},
"uid": {
"type": "string",
"example": "booking_uid_123"
},
"title": {
"type": "string",
"example": "Consultation"
},
"description": {
"type": "string",
"example": "Learn how to integrate scheduling into marketplace."
},
"hosts": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Host"
}
},
"status": {
"type": "string",
"enum": [
"cancelled",
"accepted",
"rejected",
"pending"
],
"example": "accepted"
},
"cancellationReason": {
"type": "string",
"example": "User requested cancellation"
},
"reschedulingReason": {
"type": "string",
"example": "User rescheduled the event"
},
"rescheduledFromUid": {
"type": "string",
"example": "previous_uid_123"
},
"start": {
"type": "string",
"example": "2024-08-13T15:30:00Z"
},
"end": {
"type": "string",
"example": "2024-08-13T16:30:00Z"
},
"duration": {
"type": "number",
"example": 60
},
"eventTypeId": {
"type": "number",
"example": 50,
"deprecated": true,
"description": "Deprecated - rely on 'eventType' object containing the id instead."
},
"eventType": {
"$ref": "#/components/schemas/EventType"
},
"meetingUrl": {
"type": "string",
"description": "Deprecated - rely on 'location' field instead.",
"example": "https://example.com/recurring-meeting",
"deprecated": true
},
"location": {
"type": "string",
"example": "https://example.com/meeting"
},
"absentHost": {
"type": "boolean",
"example": true
},
"createdAt": {
"type": "string",
"example": "2024-08-13T15:30:00Z"
},
"metadata": {
"type": "object",
"example": {
"key": "value"
}
},
"seatUid": {
"type": "string",
"example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1"
},
"attendees": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SeatedAttendee"
}
},
"recurringBookingUid": {
"type": "string",
"example": "recurring_uid_987"
}
},
"required": [
"id",
"uid",
"title",
"description",
"hosts",
"status",
"start",
"end",
"duration",
"eventTypeId",
"eventType",
"absentHost",
"createdAt",
"seatUid",
"attendees",
"recurringBookingUid"
]
@@ -13322,6 +13696,15 @@
"items": {
"$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13"
}
},
{
"$ref": "#/components/schemas/CreateSeatedBookingOutput_2024_08_13"
},
{
"type": "array",
"items": {
"$ref": "#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13"
}
}
],
"description": "Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects"
@@ -13332,6 +13715,243 @@
"data"
]
},
"GetSeatedBookingOutput_2024_08_13": {
"type": "object",
"properties": {
"id": {
"type": "number",
"example": 123
},
"uid": {
"type": "string",
"example": "booking_uid_123"
},
"title": {
"type": "string",
"example": "Consultation"
},
"description": {
"type": "string",
"example": "Learn how to integrate scheduling into marketplace."
},
"hosts": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Host"
}
},
"status": {
"type": "string",
"enum": [
"cancelled",
"accepted",
"rejected",
"pending"
],
"example": "accepted"
},
"cancellationReason": {
"type": "string",
"example": "User requested cancellation"
},
"reschedulingReason": {
"type": "string",
"example": "User rescheduled the event"
},
"rescheduledFromUid": {
"type": "string",
"example": "previous_uid_123"
},
"start": {
"type": "string",
"example": "2024-08-13T15:30:00Z"
},
"end": {
"type": "string",
"example": "2024-08-13T16:30:00Z"
},
"duration": {
"type": "number",
"example": 60
},
"eventTypeId": {
"type": "number",
"example": 50,
"deprecated": true,
"description": "Deprecated - rely on 'eventType' object containing the id instead."
},
"eventType": {
"$ref": "#/components/schemas/EventType"
},
"meetingUrl": {
"type": "string",
"description": "Deprecated - rely on 'location' field instead.",
"example": "https://example.com/recurring-meeting",
"deprecated": true
},
"location": {
"type": "string",
"example": "https://example.com/meeting"
},
"absentHost": {
"type": "boolean",
"example": true
},
"createdAt": {
"type": "string",
"example": "2024-08-13T15:30:00Z"
},
"metadata": {
"type": "object",
"example": {
"key": "value"
}
},
"attendees": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SeatedAttendee"
}
}
},
"required": [
"id",
"uid",
"title",
"description",
"hosts",
"status",
"start",
"end",
"duration",
"eventTypeId",
"eventType",
"absentHost",
"createdAt",
"attendees"
]
},
"GetRecurringSeatedBookingOutput_2024_08_13": {
"type": "object",
"properties": {
"id": {
"type": "number",
"example": 123
},
"uid": {
"type": "string",
"example": "booking_uid_123"
},
"title": {
"type": "string",
"example": "Consultation"
},
"description": {
"type": "string",
"example": "Learn how to integrate scheduling into marketplace."
},
"hosts": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Host"
}
},
"status": {
"type": "string",
"enum": [
"cancelled",
"accepted",
"rejected",
"pending"
],
"example": "accepted"
},
"cancellationReason": {
"type": "string",
"example": "User requested cancellation"
},
"reschedulingReason": {
"type": "string",
"example": "User rescheduled the event"
},
"rescheduledFromUid": {
"type": "string",
"example": "previous_uid_123"
},
"start": {
"type": "string",
"example": "2024-08-13T15:30:00Z"
},
"end": {
"type": "string",
"example": "2024-08-13T16:30:00Z"
},
"duration": {
"type": "number",
"example": 60
},
"eventTypeId": {
"type": "number",
"example": 50,
"deprecated": true,
"description": "Deprecated - rely on 'eventType' object containing the id instead."
},
"eventType": {
"$ref": "#/components/schemas/EventType"
},
"meetingUrl": {
"type": "string",
"description": "Deprecated - rely on 'location' field instead.",
"example": "https://example.com/recurring-meeting",
"deprecated": true
},
"location": {
"type": "string",
"example": "https://example.com/meeting"
},
"absentHost": {
"type": "boolean",
"example": true
},
"createdAt": {
"type": "string",
"example": "2024-08-13T15:30:00Z"
},
"metadata": {
"type": "object",
"example": {
"key": "value"
}
},
"attendees": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SeatedAttendee"
}
},
"recurringBookingUid": {
"type": "string",
"example": "recurring_uid_987"
}
},
"required": [
"id",
"uid",
"title",
"description",
"hosts",
"status",
"start",
"end",
"duration",
"eventTypeId",
"eventType",
"absentHost",
"createdAt",
"attendees",
"recurringBookingUid"
]
},
"GetBookingOutput_2024_08_13": {
"type": "object",
"properties": {
+3
View File
@@ -0,0 +1,3 @@
export function randomNumber(): number {
return Math.floor(Math.random() * 100000);
}
-3
View File
@@ -7,8 +7,6 @@ import { FUTURE_ROUTES_ENABLED_COOKIE_NAME, FUTURE_ROUTES_OVERRIDE_COOKIE_NAME }
const ROUTES: [URLPattern, boolean][] = [
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
["/settings/developer/:path*", process.env.APP_ROUTER_SETTINGS_DEVELOPER_ENABLED === "1"] as const,
["/settings/organizations/:path*", process.env.APP_ROUTER_SETTINGS_ORG_ENABLED === "1"] as const,
["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const,
["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const,
["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const,
@@ -23,7 +21,6 @@ const ROUTES: [URLPattern, boolean][] = [
["/auth/platform/:path*", process.env.APP_ROUTER_AUTH_PLATFORM_ENABLED === "1"] as const,
["/auth/oauth2/:path*", process.env.APP_ROUTER_AUTH_OAUTH2_ENABLED === "1"] as const,
["/workflows/:path*", process.env.APP_ROUTER_WORKFLOWS_ENABLED === "1"] as const,
["/settings/teams/:path*", process.env.APP_ROUTER_SETTINGS_TEAMS_ENABLED === "1"] as const,
["/getting-started/:step", process.env.APP_ROUTER_GETTING_STARTED_STEP_ENABLED === "1"] as const,
["/apps", process.env.APP_ROUTER_APPS_ENABLED === "1"] as const,
["/bookings/:status", process.env.APP_ROUTER_BOOKINGS_STATUS_ENABLED === "1"] as const,
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
export default WithLayout({ getServerLayout: getLayout })<"L">;
@@ -1,11 +0,0 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/ee/organizations/pages/settings/attributes/attributes-edit-view";
export const generateMetadata = async () =>
await _generateMetadata(
() => "Attribute",
(t) => t("edit_attribute_description")
);
export default Page;
@@ -1,11 +0,0 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/ee/organizations/pages/settings/attributes/attributes-create-view";
export const generateMetadata = async () =>
await _generateMetadata(
() => "Attribute",
() => "Create an attribute for your team members"
);
export default Page;
@@ -1,11 +0,0 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/ee/organizations/pages/settings/attributes/attributes-list-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("attributes"),
(t) => t("attribute_meta_description")
);
export default Page;
@@ -1,5 +0,0 @@
import BillingPage, { generateMetadata } from "../../billing/page";
export { generateMetadata };
export default BillingPage;
@@ -1,4 +0,0 @@
import BillingPage, { generateMetadata } from "../../../billing/page";
export { generateMetadata };
export default BillingPage;
@@ -1,21 +0,0 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
import { type inferSSRProps } from "@lib/types/inferSSRProps";
import LegacyPage, { LayoutWrapper } from "~/settings/platform/new/create-new-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("set_up_your_platform_organization"),
(t) => t("platform_organization_description")
);
export default WithLayout({
getLayout: LayoutWrapper,
Page: LegacyPage,
getData: withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServerSideProps),
requiresLicense: true,
});
@@ -1,19 +0,0 @@
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import CreateNewView from "~/settings/platform/oauth-clients/create-new-view";
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const p = { ...params, ...searchParams };
const clientId = p?.clientId ?? "";
return await _generateMetadata(
() => `OAuth client ${!!clientId ? "updation" : "creation"} form`,
() => ""
);
};
export default WithLayout({
getLayout: null,
Page: CreateNewView,
});
@@ -1,16 +0,0 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import PlatformView from "~/settings/platform/platform-view";
export const generateMetadata = async () => {
return await _generateMetadata(
(t) => t("platform"),
(t) => t("platform_description")
);
};
export default WithLayout({
getLayout: null,
Page: PlatformView,
});
+2
View File
@@ -74,6 +74,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<script
nonce={nonce}
id="injected-head-scripts"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: process.env.NEXT_PUBLIC_HEAD_SCRIPTS,
}}
@@ -105,6 +106,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<script
nonce={nonce}
id="injected-head-scripts"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: process.env.NEXT_PUBLIC_BODY_SCRIPTS,
}}
@@ -13,9 +13,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
// FIXME: Refactor me once next-auth endpoint is migrated to App Router
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
@@ -4,8 +4,7 @@ import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-s
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { APP_NAME } from "@calcom/lib/constants";
import ApiKeysView from "~/settings/developer/api-keys-view";
import NewApiKeyButton from "~/settings/developer/components/CreateApiKeyButton";
import ApiKeysView, { NewApiKeyButton } from "~/settings/developer/api-keys-view";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -25,7 +24,7 @@ const Page = async () => {
description={t("create_first_api_key_description", { appName: APP_NAME })}
CTA={<NewApiKeyButton />}
borderInShellHeader={true}>
<ApiKeysView isAppDir={true} />
<ApiKeysView />
</SettingsHeader>
);
};
@@ -20,7 +20,7 @@ const Page = async ({ params }: PageProps) => {
const id = typeof params?.id === "string" ? params.id : undefined;
const webhook = await WebhookRepository.findByWebhookId(id);
console.log("This is the webhook", webhook);
return (
<SettingsHeader
title={t("edit_webhook")}
@@ -1,6 +1,6 @@
import { _generateMetadata } from "app/_utils";
import { WebhooksViewAppDir } from "@calcom/features/webhooks/pages/webhooks-view";
import WebhooksView from "@calcom/features/webhooks/pages/webhooks-view";
import { APP_NAME } from "@calcom/lib/constants";
export const generateMetadata = async () =>
@@ -9,8 +9,4 @@ export const generateMetadata = async () =>
(t) => t("add_webhook_description", { appName: APP_NAME })
);
const Page = () => {
return <WebhooksViewAppDir />;
};
export default Page;
export default WebhooksView;
@@ -1,11 +1,8 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { ConferencingAppsViewWebWrapper } from "@calcom/atoms/monorepo";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { Button } from "@calcom/ui";
import ConferencingView from "~/settings/my-account/conferencing-view";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -17,22 +14,12 @@ const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
const AddConferencingButton = () => {
return (
<Button color="secondary" StartIcon="plus" href="/apps/categories/conferencing">
{t("add")}
</Button>
);
};
return (
<SettingsHeader
<ConferencingAppsViewWebWrapper
title={t("conferencing")}
description={t("conferencing_description")}
CTA={<AddConferencingButton />}
borderInShellHeader={true}>
<ConferencingView />
</SettingsHeader>
add={t("add")}
/>
);
};
@@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import OrgAttributesEditPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-edit-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("attribute"),
(t) => t("edit_attribute_description")
);
export default OrgAttributesEditPage;
@@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import OrgAttributesCreatePage from "@calcom/ee/organizations/pages/settings/attributes/attributes-create-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("attribute"),
(t) => t("create_attribute_description")
);
export default OrgAttributesCreatePage;
@@ -0,0 +1,24 @@
import { _generateMetadata, getFixedT } from "app/_utils";
import OrgSettingsAttributesPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-list-view";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("attributes"),
(t) => t("attribute_meta_description")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader title={t("attributes")} description={t("attribute_meta_description")}>
<OrgSettingsAttributesPage />
</SettingsHeader>
);
};
export default Page;
@@ -0,0 +1,5 @@
import BillingPage, { generateMetadata } from "app/settings/(settings-layout)/billing/page";
export { generateMetadata };
export default BillingPage;
@@ -16,7 +16,7 @@ const Page = async () => {
return (
<SettingsHeader title={t("general")} description={t("general_description")} borderInShellHeader={true}>
<LegacyPage isAppDir={true} />
<LegacyPage />
</SettingsHeader>
);
};
@@ -19,7 +19,7 @@ const Page = async () => {
title={t("profile")}
description={t("profile_org_description")}
borderInShellHeader={true}>
<LegacyPage isAppDir={true} />
<LegacyPage />
</SettingsHeader>
);
};
@@ -15,8 +15,8 @@ const Page = async () => {
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader title={t("profile")} description={t("profile_org_description")}>
<OrgSSOView isAppDir={true} />
<SettingsHeader title={t("sso_configuration")} description={t("sso_configuration_description_orgs")}>
<OrgSSOView />
</SettingsHeader>
);
};
@@ -19,7 +19,7 @@ const Page = async () => {
title={t("booking_appearance")}
description={t("appearance_team_description")}
borderInShellHeader={false}>
<LegacyPage isAppDir={true} />
<LegacyPage />
</SettingsHeader>
);
};
@@ -1,7 +1,9 @@
import { _generateMetadata, getFixedT } from "app/_utils";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/other-team-members-view";
import LegacyPage, {
TeamMembersCTA,
} from "@calcom/features/ee/organizations/pages/settings/other-team-members-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
@@ -15,9 +17,11 @@ const Page = async () => {
const t = await getFixedT(session?.user.locale || "en");
return (
// TODO: Add CTA Button
<SettingsHeader title={t("team_members")} description={t("members_team_description")}>
<LegacyPage isAppDir={true} />
<SettingsHeader
title={t("team_members")}
description={t("members_team_description")}
CTA={<TeamMembersCTA />}>
<LegacyPage />
</SettingsHeader>
);
};
@@ -16,7 +16,7 @@ const Page = async () => {
return (
<SettingsHeader title={t("profile")} description={t("profile_team_description")}>
<LegacyPage isAppDir={true} />
<LegacyPage />
</SettingsHeader>
);
};
@@ -16,7 +16,7 @@ const Page = async () => {
return (
<SettingsHeader title={t("org_admin_other_teams")} description={t("org_admin_other_teams_description")}>
<LegacyPage isAppDir={true} />
<LegacyPage />
</SettingsHeader>
);
};
@@ -19,7 +19,7 @@ const Page = async () => {
title={t("booking_appearance")}
description={t("appearance_team_description")}
borderInShellHeader={false}>
<LegacyPage isAppDir={true} />
<LegacyPage />
</SettingsHeader>
);
};
@@ -0,0 +1,4 @@
import BillingPage, { generateMetadata } from "app/settings/(settings-layout)/billing/page";
export { generateMetadata };
export default BillingPage;
@@ -0,0 +1,27 @@
import { _generateMetadata, getFixedT } from "app/_utils";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import TeamBookingLimitsView from "@calcom/features/ee/teams/pages/team-booking-limits-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("booking_limits"),
(t) => t("booking_limits_team_description")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader
title={t("booking_limits")}
description={t("booking_limits_team_description")}
borderInShellHeader={false}>
<TeamBookingLimitsView />
</SettingsHeader>
);
};
export default Page;
@@ -16,7 +16,7 @@ const Page = async () => {
return (
<SettingsHeader title={t("team_members")} description={t("members_team_description")}>
<LegacyPage isAppDir={true} />
<LegacyPage />
</SettingsHeader>
);
};
@@ -19,7 +19,7 @@ const Page = async () => {
title={t("profile")}
description={t("profile_team_description")}
borderInShellHeader={true}>
<LegacyPage isAppDir={true} />
<LegacyPage />
</SettingsHeader>
);
};
@@ -16,7 +16,7 @@ const Page = async () => {
return (
<SettingsHeader title={t("teams")} description={t("create_manage_teams_collaborative")}>
<LegacyPage isAppDir={true} />
<LegacyPage />
</SettingsHeader>
);
};
@@ -1,15 +1,14 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import EditView from "~/settings/platform/oauth-clients/[clientId]/edit/edit-view";
import MembersPage from "~/members/members-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("oAuth_client_updation_form"),
() => ""
(t) => t("organization_members"),
(t) => t("organization_description")
);
export default WithLayout({
getLayout: null,
Page: EditView,
Page: MembersPage,
});

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