Compare commits

...
117 Commits
Author SHA1 Message Date
Anik Dhabal Babu 2cfc63e6e3 fix: Reschedule email for RR event shows wrong timezone to attendee (#18262)
Post release / release (push) Canceled after 0s
2024-12-20 14:02:03 -06:00
GitHub Actions 4f66f3d2c6 chore: release v4.8.7 2024-12-20 20:01:15 +00:00
Morgan ca3e7bd97d fix: hotfix hide hidden booking fields in event-types GET / (#18306) 2024-12-20 19:37:32 +00:00
Benny Joo cafa3f9cf3 fix: make sure to call refresh for pages rendered by app router (#18304) 2024-12-20 18:44:35 +00:00
Benny Joo e04faffcbb chore: add cache to static pages in app router (#18278)
* add cache

* fix

* refactor
2024-12-20 13:37:20 -05:00
Peer Richelsen 6f1556a2fd feat: added chatbase app (#18302) 2024-12-20 17:50:19 +00:00
mintlify[bot] d72a954896 Mintlify Docs Edits (#18301)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2024-12-20 17:10:39 +00:00
amrit abeb20373c fix portugese (pt-BR) password validation from a required eight to seven. (#18273) 2024-12-20 17:33:46 +05:30
MorganandRajiv Sahal 422ef2f5b8 chore: add cal.com logo to platform booker embed (#18269)
* chore: add cal.com logo to platform booker embed

* fixup! chore: add cal.com logo to platform booker embed

---------

Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2024-12-20 12:55:25 +02:00
Omar López ec920eb2a8 fix: prevents cron to be clogged with calendars with missing credentialId (#18281) 2024-12-19 23:53:51 +00:00
GitHub Actions 795eecf300 chore: release v4.8.6
Post release / release (push) Canceled after 0s
2024-12-19 22:42:27 +00:00
Calcom BotandReplexica 8ae3d6d55b feat: update translations via @replexica (#18279)
Co-authored-by: Replexica <support@replexica.com>
2024-12-19 20:51:57 +00:00
85070574da feat: Support event level conflict checking (#18052)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Omar López <zomars@me.com>
2024-12-19 19:34:24 +00:00
Morgan d1c2872f6e fix: platform booker timezone select (#18277) 2024-12-19 17:08:34 +00:00
Morgan 42d78f8099 fix: platform booker icons and styling (#18276) 2024-12-19 16:45:05 +00:00
Lauris Skraucis f0d7a757d4 fix: Booker atom default booking fields (#18275)
* refactor: rename transformBookingFieldsApiToInternal to transformBookingFieldsApiRequestToInternal

* refactor: default booking fields can required or not required

* fix: render system fields in booker

* fix: hidden property of booking fields

* fix: default field labels and placeholders

* chore: bump platform libraries

* fix: tests

* fix: e2e tests
2024-12-19 16:30:14 +00:00
GitHub Actions 0b2f09ab7d chore: release v4.8.5
Post release / release (push) Canceled after 0s
2024-12-19 15:10:13 +00:00
Benny Joo 355aaa7010 fix: include app directory in rewrites generation logic + migrate /reschedule pages to App Router (#18261)
* chore: app router - /reschedule pages (#18150)

* app router - /reschedule pages

* add to config.matcher

* generateMetdata is not needed

* include app directory in rewrites generation logic

* make it more readable

* make code more elegant

* fix

* fix

* fix

* fix

* test and refactor
2024-12-19 06:22:19 -07:00
Rajiv Sahal c27b73aa9a fix: event type atom availability (#18263) 2024-12-19 16:38:05 +05:30
Somay Chauhan a622c7b624 fix: seats option not working for booker atom (#18265) 2024-12-19 08:07:40 +00:00
GitHub Actions 19bd3e5849 chore: release v4.8.4
Post release / release (push) Canceled after 0s
2024-12-18 23:21:28 +00:00
Benny Joo 7827028e0a Revert "chore: app router - /reschedule pages (#18150)" (#18258)
This reverts commit a7f24e777b.
2024-12-18 22:48:03 +00:00
Joe Au-Yeung ebc3ced687 chore: CRM logging, add booking uid (#18256)
* Add more logging

* Add conditional
2024-12-18 21:23:04 +01:00
Nizzy b56824e626 removed posthog identifier (#18253) 2024-12-18 15:03:17 -05:00
Benny Joo 460e511459 chore: app router - /booking pages (#18182)
* chore: app router - /booking pages

* move pages to /app

* add to config matcher
2024-12-18 14:09:34 -05:00
Benny Joo b4fd6f3442 chore: bring back removed onboarding steps (#18254)
* add connected-calendar and connected-video steps back

* adapt test

* fix name
2024-12-18 20:05:17 +01:00
Benny Joo 0a7a0e3687 chore: app router - /teams single page (#18184)
* chore: app router - /teams single page

* fix type check
2024-12-18 17:51:01 +00:00
Calcom BotandReplexica 595fffbea0 feat: update translations via @replexica (#18249)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2024-12-18 17:44:50 +00:00
GitHub Actions 218c9152da chore: release v4.8.3
Post release / release (push) Canceled after 0s
2024-12-18 17:22:22 +00:00
Omar López 77fe23da49 fix: skip failed cache subscriptions from retrying (#18252) 2024-12-18 17:16:59 +00:00
sean-brydon a583db3fd4 refactor and fix bulk attribute handling (#18231) 2024-12-18 16:31:03 +00:00
cb1f803d0a feat: ooo calibration (#17864)
* first version of ooo calibration

* decrease precision when checking for full day event

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* improve variable naming

* don't include hosts created after OOO

* fix failing tests

* fix all tests

* add test for ooo calibration

* first implementation to fix full day busy event reaching the current month

* allow mutiple day events

* make getAvailabilityWithTimeZones optional

* fix type error

* fix type errors

* fix tests

* fix type error

* fix test

* code clean up

* remove time adjustment from map

* code clean up

* fix tests

* getAllCalendars -> this.getAllCalendars

* fix typo

* extract mergeOverlappingRanges

* code improvements from feedback

* fix getCalIdsWithTimeZone

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2024-12-18 16:26:59 +00:00
Peer Richelsen 479d6b5961 Update i18n.json 2024-12-18 16:28:22 +01:00
Alex van Andel 4ab3ec73c0 chore: Set the stage for a primary key with value uuid() (#18245)
* chore: Set the stage for a primary key  with value uuid()

* fix: Test and ensure it validates the id value is set and defined

* Add unique index manually, prisma doesn't support this

* revert unique
2024-12-18 15:14:04 +00:00
Joe Au-Yeung 8dda418127 feat: Salesforce skip ownership check if attendee email is a free domain (#17916)
* Add email domain array

* Create numbered email domain object

* Check email domain

* Rename function

* Add tests

* Frontend enable skip ownership check if free email domain

* Backend ignore adding ownership to return records if free email domain check is enabled

* feat: Only require confirmation for free email domains (#17917)

* Add requiresConfirmationForFreeEmail to db

* Add option to event type settings

* Get requiresConfirmationForFreeEmail for event type page

* Include requiresConfirmationForFreeEmail in fetching event type

* Pass bookerEmail to `getRequiresConfirmationFlags`

* Add free email domain check to `determineRequiresConfirmation`

* Add `requiresConfirmationForFreeEmail` to types

* Add severity to Watchlist table

* Add migration for watchlist severity

* Add `getEmailDomainInWatchlist` method to watchlist repository

* Use watchlist repository to check for free email domain

* Mock watchlist repository in test

* Update test

* Rename method

* Add severity to blocked list

* Move check free email domain to async

* Type checks

* Adjust for promise returned

* Fix tests

* Fix

* Fix tests
2024-12-18 15:02:42 +00:00
Benny Joo a7f24e777b chore: app router - /reschedule pages (#18150)
* app router - /reschedule pages

* add to config.matcher

* generateMetdata is not needed
2024-12-18 14:54:15 +01:00
Benny Joo f78fca594a chore: app router - /getting-started page (#18185)
* remove env vars

* move pages to /app

* remove Head component

* remove pages router

* add to config matcher

* fix
2024-12-18 14:41:03 +01:00
Lauris Skraucis e6a7ae7da3 fix: platform decrease usage when cancelling booking (#18241)
* fix: decrement billing upon cancelled booking

* docs: explain billing in FAQ

* update docs
2024-12-18 14:50:31 +02:00
Lauris Skraucis ad5d92dff0 refactor: v2 calendars busy-times (#18244)
* refactor: make dateFrom and dateTo required

* refactor: make dateFrom and dateTo required

* refactor: useCalendarsBusyTimes enabled only if dateFrom & dateTo passed
2024-12-18 13:36:26 +01:00
Anik Dhabal Babu 8bf46ab8b6 fix: missing n8n app screenshot and add margin to the back button (#18240)
* fix: n8n app screenshot missing

* remove log
2024-12-18 12:35:26 +00:00
sean-brydon 34b0753f8d feat: match edit user sheet designs (#18243)
* edit sheet design changes

* remove attribute header
2024-12-18 11:50:19 +00:00
Calcom BotandReplexica 0bd7227150 feat: update translations via @replexica (#18234)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2024-12-17 23:22:10 -05:00
Omar López 4d192ee328 fix: single rejection caused cache cron to fail (#18239) 2024-12-17 23:04:08 -05:00
63013d402e feat: Plain integration (#18130)
* revert: "fix: Request permissions to allow events to be created on shared Office365/Outlook calendars (#17760)"

This reverts commit 1680cba5a5.

* plain.com card

* detiled error handling for get customer

* working email and id

* hmac

* Revert "fix: correct line-breaks in calendar event description (#18077)"

This reverts commit 06494a6999.

* pr changes requet

* remove pan for now

* add-new-implementation-for-early-review

* Pushing fix for createHmac stringify

* added validation and user repository for email check

* add apiRouteMiddleware which handles the error handling

* HMAC_SECRET_KEY -> PLAIN_HMAC_SECRET_KEY

* Use the right error

* Convey right error to consumer

* Fixup apiRouteMiddleware to handle handler

* Don't export handler, only export POST

* changed to app directory

* working unkown user card

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-17 20:24:07 -05:00
Lauris Skraucis 6b3ccbc61a fix: v2 isSystemAdmin (#18162)
* refactor: ApiAuthStrategy return isSystemAdmin and define returned type

* refactor: use ApiAuthGuardUser instead of GetUserReturnType

* regenerate docs
2024-12-17 19:32:01 +00:00
Lauris SkraucisandSomay Chauhan 2af5cba0d7 feat: v2 teams event types (#18085)
* feat: v2 teams event-types endpoints

* refactor: rely on teams event types service and repository

* finish mergin main into this branch

* test: teams event types

* refactor: move teams event types output from org to teams folder

* refactor: move teams event types output from org to teams folder

* chore: finish merge main

* refactor: logger name and unused import

* chore: finish main merge

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-12-17 20:25:42 +01:00
Omar LópezandKeith Williams ae7bbaf414 fix: calendar cache date ranges (#18237)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-12-17 19:20:19 +00:00
Hariom Balhara 4df4383e1e fixes:CAL-4905 (#18222) 2024-12-17 22:51:42 +05:30
GitHub Actions a0706d9b51 chore: release v4.8.2
Post release / release (push) Canceled after 0s
2024-12-17 17:05:26 +00:00
Omar LópezandKeith Williams 2eb385af48 feat: allows forcing/skipping calendar cache serving (#18224)
* feat: allows forcing/skipping calendar cache serving

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

* Update features.repository.ts

* Added to HNB

* type fixes

* Update packages/prisma/migrations/20241216000000_add_calendar_cache_serve/migration.sql

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

* Update packages/prisma/zod-utils.ts

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

* Update selectedCalendar.ts

---------

Signed-off-by: Omar López <zomars@me.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-12-17 16:54:55 +00:00
Lauris Skraucis a0f986f949 feat: v2 teams CRUD (#18019)
* feat: teams endpoints

* refactor: organization teams reuse team types

* refactor: move team specific functions out of organizations-teams.repository

* supporting files

* regenerate docs

* feat: stripe service to generate team creation checkout

* refactor: request payment when creating a team

* fix: tests

* fix env

* driveby: event type name and email booking fields

* finish merge main
2024-12-17 14:19:57 +00:00
Keith Williams b0094bad2d perf: More tracing for handleNewBooking (#18233) 2024-12-17 12:29:41 +00:00
Rajiv Sahal c732c06835 fix: bookerEmbed atom (#18168)
* update translations

* add custom hook for platform embed

* update event type ouput DTO response

* fixup

* display terms and conditions for embed

* update v2 docs

* PR feedback

* update docs

* PR feedback

* update docs

* update output DTO

* update docs

* update output typing

* docs update

* resolve merge conflicts

* merge conflicts resolving

* resolve merge conflicts

* fixup

* update docs
2024-12-17 12:07:47 +00:00
Anik Dhabal Babu c1334475c5 fix: private team does not hide team members from manged event type (#18166) 2024-12-17 17:05:15 +05:30
Lauris Skraucis b35dc43011 feat: v2 wildcard redirectURIs (#18165)
* feat: isOriginAllowed function

* feat: use isOriginAllowed in v2

* refactor: shorten isOriginAllowed
2024-12-17 11:21:59 +00:00
Morgan c7dc3ecc49 chore: add TLD in username of managed users (#18229)
* chore: add TLD in username of managed users

* fixup! chore: add TLD in username of managed users

* bump platform libraries
2024-12-17 12:45:48 +02:00
Somay Chauhan a613c975d3 refactor: added ApiPropertyOptional decorator for optional fields in api for better documentation (#18208)
* update decorators 1

* update decorators 2

* update decorators 3

* update decorators 4

* update decorators 4

* update decorators 6

* update decorators 7

* added documentation,json and openapi.json

* implemented feedback

* remove default values from update dto's

* update

* removed default from redirectUris
2024-12-17 10:29:07 +00:00
Morgan 5fad7c9478 fix: redirect on calendar oauth denied auth (#18228) 2024-12-17 09:05:10 +00:00
Udit Takkar 318470d866 fix: webhook improvement (#18220) 2024-12-17 14:32:38 +05:30
Morgan a0ce42e375 chore: improve prisma exception filter messages (#18227)
* chore: improve prisma exception filter messages

* fixup! chore: improve prisma exception filter messages
2024-12-17 08:40:19 +00:00
Anik Dhabal Babu 5293a5cc54 fix: column f.userid does not exist (#18215) 2024-12-17 08:37:48 +00:00
Benny Joo 42aefc9032 perf: limit # of host members for OG image (#18219)
* fetch only 5 hosts and only first user

* refactor

* limit to 3
2024-12-17 08:15:11 +00:00
Keith Williams 14acf4e9bc chore: New booking perf tracing (#18212)
* chore: Add perf tracing for handleNewBooking

* Added a few more to the event manager

* Added the sentry wrapper that enables perf tracing

* Revert "Added a few more to the event manager"

This reverts commit ccf3ee5200466b8b1d51f5663363fbb9dfd9a06f.

* Fixed where we wrap sentry

* Reverse sentry wrapper
2024-12-17 08:51:00 +01:00
Lauris Skraucis 80815bd8a5 feat: Booker atom slots callbacks (#18203) 2024-12-17 12:58:18 +05:30
Benny Joo 9ed8face03 Revert "feat: remove setup-availability step in onboarding flow (#17799)" (#18205)
This reverts commit a466e8eef7.
2024-12-16 22:03:41 +00:00
Udit Takkar 18a107d081 fix: trigger guest no show bug (#18164)
* fix: trigger guest no show

* test: add tests for trigger guest no show
2024-12-16 11:43:03 -05:00
Alex van Andel 04d7537e2f perf: improve insights where query and type fix (#18211)
* perf: insights perf improvement

* removed additional const
2024-12-16 15:50:20 +00:00
Udit Takkar 423b17a819 refactor: getContacts (#18137) 2024-12-16 10:17:59 -05:00
GitHub Actions 0466b7ebd6 chore: release v4.8.1
Post release / release (push) Canceled after 0s
2024-12-16 14:35:27 +00:00
Lauris Skraucis 09329707c1 fix: bookings parsing errors (#18200) 2024-12-16 16:31:04 +02:00
Anik Dhabal Babu 1773aba378 fix: fix data expose issue in routing form insights (#18207) 2024-12-16 15:30:25 +01:00
Somay ChauhanandMorgan f8be2b3833 feat: added office 365 video to conferencing atoms (#18067)
* feat: added office 365 video to conferencing atoms

* added documentation for conferencing atoms

* added props table to the documentation

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-12-16 16:20:59 +02:00
Eunjae Leeandsean-brydon 79d488faca feat: insights filter refactoring (#18167)
* Reapply "feat: add text & numeric filters to insights routing responses (#18016)"

This reverts commit 49070b96ae.

* support SINGLE_SELECT

* fix type error

* put back ts-expect-error

* remove playground

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-12-16 13:47:28 +00:00
Venkata Rohan KambhampatiandUdit Takkar da2c57c302 fix: Booking questions order correction in confirmation email (#18192)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-16 09:33:04 +00:00
Keith Williams abaf03adde fix: Next.js bundle analysis build error (#18193) 2024-12-15 17:40:59 +05:30
GitHub Actions 5b353c3d0c chore: release v4.8.0
Post release / release (push) Canceled after 0s
2024-12-15 11:29:06 +00:00
Benny Joo 1f4cda31ac update app layout (#17126) 2024-12-14 18:42:43 -05:00
Calcom BotandReplexica 73d1a6f468 feat: update translations via @replexica (#18158)
Co-authored-by: Replexica <support@replexica.com>
2024-12-14 19:05:59 +00:00
Benny JooandAlex van Andel f72748b6f2 fix: trpc/react-query — only one query client should exist (#18179)
* use prefetch function in ScheduleListItem and refactor

* modify app router trpc provider to match the original configs

* revert unneeded change

* move trpc provider up and do refactors

* remove log

* add comment

* fix: Some typing style fixes

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-13 20:26:51 -05:00
Benny Joo d12fc78fea chore: add viewport / favicon metadata (#18177)
* fix favicon name

* add viewport metadata for PageWrapperAppDir

* fix

* fix

* refactor
2024-12-13 20:25:03 +00:00
Carina WollendorferandCarinaWolli 3d26332443 fix: change trigger time for form submitted not booked webhook (#18176)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-12-13 20:16:11 +00:00
Omar López bdb490c894 Revert "fix: correct line-breaks in calendar event description" (#18175) 2024-12-13 20:03:56 +00:00
Morgan 01dbc686b9 chore: bump platform libraries version for api-v2 (#18174) 2024-12-13 19:35:18 +00:00
Carina WollendorferandCarinaWolli 376961f30d fix: don't send reassingment email to attendee for past events (#18173)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-12-13 19:02:54 +00:00
Morgan 18a0eb9736 fix: improve getAllUserBooking filter by teamIds (#18171)
* fix: improve getAllUserBooking filted by teamIds

* fixup! fix: improve getAllUserBooking filted by teamIds
2024-12-13 20:18:58 +02:00
Carina WollendorferandCarinaWolli 84fb4f68b2 fix: wrong workflow notifications when enabling workflows (#18169)
* only schedule workflow emails not send

* fix if statement

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-12-13 11:58:30 -05:00
Udit Takkar b5ab6fe3fa fix: add no show guest (#18075)
* fix: add booker no show

* chore: update name
2024-12-13 16:00:17 +00:00
Anik Dhabal BabuandUdit Takkar 645985c783 fix: Invalid time zone specified: Etc/Unknown (#18121)
* fix: Invalid time zone specified: Etc/Unknown

* update

* Update PaymentPage.tsx

* update

* update one more file

* address review

* update test

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-13 15:29:02 +00:00
GitHub Actions a84513eda5 chore: release v4.7.25
Post release / release (push) Canceled after 0s
2024-12-13 14:19:52 +00:00
Benny Joo b97331aeb3 chore: /availability - Remove Pages Router / Render App Router (#16517)
* /availability: Remove Pages Router / Render App Router

* comment out server side fetching

* fix

* refactor for safer code

* add to config.matcher
2024-12-13 12:44:57 +00:00
Morgan 851d96423b chore: small changes for platform booker embed (#18159) 2024-12-13 13:24:19 +01:00
Anik Dhabal BabuandPeer Richelsen ef830f11ec fix: Hyperlinks not highlighted in eventmeta (#18120)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-12-13 13:23:25 +01:00
Keith Williams 1ab4ec9c22 chore: Upgrade actions/upload-artifact to v4 (#18160) 2024-12-13 11:32:50 +00:00
Benny Joo 656a5bf423 chore: remove hardcoded strings (#18156) 2024-12-13 09:54:28 +01:00
Benny Joo 8a6450e854 chore: use getTranslate instead (#18148) 2024-12-13 13:06:47 +05:30
Benny Joo 51841e5e9c fix: ensure migrated pages are added to config.matcher in middleware (#18153) 2024-12-13 12:43:08 +05:30
Benny Joo 59dfcaa2fa chore: fix misleading test names (#18154) 2024-12-13 04:32:52 +00:00
Omar López 0d24706380 refactor: improved observability in google calendar service (#18147) 2024-12-13 03:47:43 +00:00
Alex van Andel 87b6e0b49c fix: Prisma validation error in generateMetadata (workflows) (#18149) 2024-12-13 03:33:19 +00:00
Calcom BotandReplexica 4244ca3117 feat: update translations via @replexica (#18139)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2024-12-12 20:48:07 -05:00
Carina WollendorferandCarinaWolli 8d0f0d4899 fix: default to 100 when attribute weight undefined (#18145)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-12-12 22:57:43 +00:00
Anik Dhabal Babu 6463983d55 fix: Searching users in admin page not working (#18140) 2024-12-12 19:13:00 +00:00
Rajiv Sahal c8661563c8 fix: update get booking output for v2 (#18082)
* also include email field for attendees

* update tests

* update tests

* updating more tests

* fixup

* fixup

* fixup fixup
2024-12-12 18:55:28 +00:00
Sanchit Tewari 2262d56491 fix: show tooltip below the delete button (#18135) 2024-12-12 18:53:25 +00:00
GitHub Actions 6b1c747c0e chore: release v4.7.24
Post release / release (push) Canceled after 0s
2024-12-12 17:22:03 +00:00
Benny Joo 04036c32a7 fix: filling users logic (#18128)
* fix filling users logic

* fix type check

* refactor
2024-12-12 17:12:56 +00:00
Alex van Andel 49070b96ae Revert "feat: add text & numeric filters to insights routing responses (#18016)"
This reverts commit a7e7561efa.
2024-12-12 17:10:49 +00:00
GitHub Actions 237fea0a41 chore: release v4.7.23
Post release / release (push) Canceled after 0s
2024-12-12 16:40:04 +00:00
a7e7561efa feat: add text & numeric filters to insights routing responses (#18016)
* feat: improve text filters (WIP)

* move function to bottom

* apply some styles

* fix selection of TextFilterOptions

* rename value to operand

* remove unused file

* merge filters/filters into filters/utils

* fix regression of not putting url params correctly

* move makeWhereClause to filters/utils

* fix negative, empty, and not empty operators

* fix initial filtering from search state (url)

* fix type errors

* do not send an empty array to query

* update yarn.lock

* i18n for text filter operators

* extract logic as useColumnFilters()

* add missing import

* fix type error

* revert yarn.lock

* use i18n

* insensitive text match

* move data-table to @calcom/features

* fix type errors

* fix type errors

* fix type errors

* feat: support DataTable filters for Insights Routing WIP

* remove unused filters

* remove additionalFilters and fix types

* clean up filter components

* support icons for ActiveFilters

* support filters on json

* fix filter ui

* fix type error and clean up

* revert changes

* revert change

* clean up

* revert change

* fix compatibility with insights booking page

* remove unused params

* fix type errors

* update yarn.lock

* fix field filter and adjust ui

* chore: update yarn.lock

* fix text filter

* add Clear Filters button

* add more test data

* feat: support numeric filter WIP

* feat: support numeric filter

* feat: move People to an external filter

* fixing query states

* rename state to activeFilters

* add useDataTable

* fix ResponseCellValue bug and enable resizing for RoutingFormResponsesTable

* fix response values

* truncate attribute text

* seed booking attendees

* fix accessor (no actual change though)

* add a gap

* rename

* use pathname as default identifier for DataTable

* fix type error

* support text filter for assignment reason

* Apply suggestions from code review

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* rename fields to headers

* use safeParse

* fix type error

* push yarn.lock with faker

* fix MemberList

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: sean-brydon <sean@cal.com>
2024-12-12 16:06:30 +00:00
Alex van Andel e366c10521 Convert update statement into unique select in getServerSession 2024-12-12 15:50:44 +00:00
Morgan 2ace1d1017 chore: add successRedirectUrl to event-types api v2 (#18136)
* chore: add successRedirectUrl to event-types api v2

* chore: add successRedirectUrl to event-types api v2 patch

* chore: add successRedirectUrl to event-types api v2 patch
2024-12-12 16:32:35 +02:00
sean-brydon fbe31ad82d early return if no users are found to prevent prisma error (#18133) 2024-12-12 13:37:39 +01:00
sean-brydonandUdit Takkar ee3c08918a feat: weights per attribute UI (#18109)
* wip attribut weights

* edit sheet and display in table

* add shrink so badges dont split

* push changes to support weights on csv export

* fix typing issue

* type error

* fall back to 100 weight

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-12 11:54:10 +00:00
Lauris Skraucis b6d6b7cdb4 fix: store default booking fields when creating event (#18108)
* refactor: store default booking fields when creating event

* Revert "refactor: store default booking fields when creating event"

This reverts commit 78b7c4eda2be61420b4410bc80a3d77b5557a967.

* refactor: v2 return default booking fields if they are null

* test: v2 returning default booking fields if they are null

* chore: platform-libraries export getBookingFieldsWithSystemFields
2024-12-12 10:58:45 +01:00
Hariom BalharaandUdit Takkar 1b82552e76 Fix crm error log (#18105)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-12 13:15:14 +05:30
Joe Au-Yeung 13a90aba30 Fix logic (#18131) 2024-12-11 21:03:51 -07:00
Alex van Andel de0de64a16 build: Node alpine libssl symlink fix (#18127) 2024-12-12 00:39:36 +00:00
567 changed files with 26108 additions and 4549 deletions
+6 -4
View File
@@ -107,6 +107,12 @@ NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=
# plain.com config
PLAIN_API_KEY=
PLAIN_API_URL=https://api.plain.com/v1
PLAIN_HMAC_SECRET_KEY=
# Zendesk Config
NEXT_PUBLIC_ZENDESK_KEY=
@@ -355,13 +361,9 @@ APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
APP_ROUTER_BOOKING_ENABLED=0
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
APP_ROUTER_APPS_ENABLED=0
APP_ROUTER_TEAM_ENABLED=0
APP_ROUTER_TEAMS_ENABLED=0
APP_ROUTER_AVAILABILITY_ENABLED=0
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
APP_ROUTER_AUTH_LOGIN_ENABLED=0
APP_ROUTER_AUTH_LOGOUT_ENABLED=0
+3 -2
View File
@@ -17,7 +17,6 @@ env:
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
@@ -51,7 +50,8 @@ env:
jobs:
analyze:
if: always()
runs-on: buildjet-2vcpu-ubuntu-2204
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
@@ -60,6 +60,7 @@ jobs:
- name: Analyze bundle
run: |
cd apps/web
export NODE_OPTIONS="--max_old_space_size=8192"
npx -p nextjs-bundle-analysis@0.5.0 report
- name: Upload bundle
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
printf '{
"pr_number": ${{ github.event.pull_request.number }}
}' >> context.json
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: context.json
path: ./
View File
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"
+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"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- calcom-monorepo
+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"
+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"
@@ -61,6 +61,8 @@ describe("rateLimitApiKey middleware", () => {
reset: Date.now(),
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => {
onRateLimiterResponse(rateLimiterResponse);
});
+2 -2
View File
@@ -1,9 +1,9 @@
import type { NextApiRequest } from "next";
import { checkIfEmailInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
import { checkIfEmailIsBlockedInWatchlistController } 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));
return user.locked || (await checkIfEmailIsBlockedInWatchlistController(user.email));
}
@@ -9,11 +9,25 @@ export const schemaSelectedCalendarBaseBodyParams = SelectedCalendar;
export const schemaSelectedCalendarPublic = SelectedCalendar.omit({});
export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams.partial({
userId: true,
});
export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams
.partial({
userId: true,
})
.omit({
// id will be set by the database
id: true,
// No eventTypeId support in API v1
eventTypeId: true,
});
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams.partial();
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams
.omit({
// id is decided by DB
id: true,
// No eventTypeId support in API v1
eventTypeId: true,
})
.partial();
export const selectedCalendarIdSchema = schemaQueryIdAsString.transform((v, ctx) => {
/** We can assume the first part is the userId since it's an integer */
+2 -1
View File
@@ -14,7 +14,8 @@
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"start": "PORT=3003 next start",
"docker-start-api": "PORT=80 next start",
"type-check": "tsc --pretty --noEmit"
"type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit"
},
"devDependencies": {
"@calcom/tsconfig": "*",
@@ -1,9 +1,10 @@
import type { NextApiRequest } from "next";
import type { UserWithCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
import { getConnectedDestinationCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
import { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/lib/getConnectedDestinationCalendars";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { UserRepository } from "@calcom/lib/server/repository/user";
import prisma from "@calcom/prisma";
import { extractUserIdsFromQuery } from "~/lib/utils/extractUserIdsFromQuery";
@@ -104,12 +105,8 @@ async function getHandler(req: NextApiRequest) {
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
const usersWithCalendars = await prisma.user.findMany({
where: { id: { in: userIds } },
include: {
selectedCalendars: true,
destinationCalendar: true,
},
const usersWithCalendars = await UserRepository.findManyByIdsIncludeDestinationAndSelectedCalendars({
ids: userIds,
});
return await getConnectedCalendars(usersWithCalendars);
@@ -117,11 +114,12 @@ async function getHandler(req: NextApiRequest) {
async function getConnectedCalendars(users: UserWithCalendars[]) {
const connectedDestinationCalendarsPromises = users.map((user) =>
getConnectedDestinationCalendars(user, false, prisma).then((connectedCalendarsResult) =>
connectedCalendarsResult.connectedCalendars.map((calendar) => ({
userId: user.id,
...calendar,
}))
getConnectedDestinationCalendarsAndEnsureDefaultsInDb({ user, onboarding: false, prisma }).then(
(connectedCalendarsResult) =>
connectedCalendarsResult.connectedCalendars.map((calendar) => ({
userId: user.id,
...calendar,
}))
)
);
const connectedDestinationCalendars = await Promise.all(connectedDestinationCalendarsPromises);
@@ -1,7 +1,7 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
@@ -49,7 +49,9 @@ import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
export async function deleteHandler(req: NextApiRequest) {
const { query } = req;
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
await prisma.selectedCalendar.delete({ where: { userId_integration_externalId } });
await SelectedCalendarRepository.deleteUserLevel({
where: userId_integration_externalId,
});
return { message: `Selected Calendar with id: ${query.id} deleted successfully` };
}
@@ -1,7 +1,7 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import { schemaSelectedCalendarPublic, selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
@@ -49,8 +49,8 @@ import { schemaSelectedCalendarPublic, selectedCalendarIdSchema } from "~/lib/va
export async function getHandler(req: NextApiRequest) {
const { query } = req;
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
const data = await prisma.selectedCalendar.findUniqueOrThrow({
where: { userId_integration_externalId },
const data = await SelectedCalendarRepository.findUserLevelUniqueOrThrow({
where: userId_integration_externalId,
});
return { selected_calendar: schemaSelectedCalendarPublic.parse(data) };
}
@@ -3,6 +3,8 @@ import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import type { UpdateArguments } from "@calcom/lib/server/repository/selectedCalendar";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import prisma from "@calcom/prisma";
import {
@@ -56,7 +58,7 @@ export async function patchHandler(req: NextApiRequest) {
const { query, isSystemWideAdmin } = req;
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
const { userId: bodyUserId, ...data } = schemaSelectedCalendarUpdateBodyParams.parse(req.body);
const args: Prisma.SelectedCalendarUpdateArgs = { where: { userId_integration_externalId }, data };
const args: UpdateArguments = { where: { ...userId_integration_externalId }, data };
if (!isSystemWideAdmin && bodyUserId)
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
@@ -67,7 +69,7 @@ export async function patchHandler(req: NextApiRequest) {
args.data.userId = bodyUserId;
}
const result = await prisma.selectedCalendar.update(args);
const result = await SelectedCalendarRepository.updateUserLevel(args);
return { selected_calendar: schemaSelectedCalendarPublic.parse(result) };
}
@@ -1,9 +1,9 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import type { FindManyArgs } from "@calcom/lib/server/repository/selectedCalendar";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import { schemaSelectedCalendarPublic } from "~/lib/validations/selected-calendar";
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
@@ -34,7 +34,7 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
async function getHandler(req: NextApiRequest) {
const { userId, isSystemWideAdmin } = req;
/* Admin gets all selected calendar by default, otherwise only the user's ones */
const args: Prisma.SelectedCalendarFindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
const args: FindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
/** Only admins can query other users */
if (!isSystemWideAdmin && req.query.userId)
@@ -46,7 +46,7 @@ async function getHandler(req: NextApiRequest) {
if (Array.isArray(query.userId)) args.orderBy = { userId: "asc" };
}
const data = await prisma.selectedCalendar.findMany(args);
const data = await SelectedCalendarRepository.findManyUserLevel(args);
return { selected_calendars: data.map((v) => schemaSelectedCalendarPublic.parse(v)) };
}
@@ -3,6 +3,7 @@ import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import prisma from "@calcom/prisma";
import {
@@ -52,7 +53,11 @@ import {
async function postHandler(req: NextApiRequest) {
const { userId, isSystemWideAdmin } = req;
const { userId: bodyUserId, ...body } = schemaSelectedCalendarBodyParams.parse(req.body);
const args: Prisma.SelectedCalendarCreateArgs = { data: { ...body, userId } };
const args: {
data: Prisma.SelectedCalendarUncheckedCreateInput;
} = {
data: { ...body, userId },
};
if (!isSystemWideAdmin && bodyUserId)
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
@@ -63,7 +68,7 @@ async function postHandler(req: NextApiRequest) {
args.data.userId = bodyUserId;
}
const data = await prisma.selectedCalendar.create(args);
const data = await SelectedCalendarRepository.create(args.data);
return {
selected_calendar: schemaSelectedCalendarPublic.parse(data),
+4 -1
View File
@@ -52,6 +52,8 @@ describe("POST /api/attendees", () => {
bookingId: 1,
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
prismaMock.attendee.create.mockResolvedValue(attendeeData);
req.isSystemWideAdmin = true;
req.userId = 123;
@@ -84,7 +86,8 @@ describe("POST /api/attendees", () => {
timeZone: "UTC",
bookingId: 1,
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
prismaMock.attendee.create.mockResolvedValue(attendeeData);
req.userId = 123;
+1 -1
View File
@@ -11,7 +11,7 @@ import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
import prisma from "@calcom/prisma";
import type { Booking } from "@calcom/prisma";
import type { Booking } from "@calcom/prisma/client";
import handler from "../../../pages/api/bookings/_post";
@@ -21,6 +21,8 @@ describe("DELETE /api/event-types/[id]", () => {
beforeEach(() => {
vi.resetAllMocks();
// Mocking membership.findFirst
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
prismaMock.membership.findFirst.mockImplementation(({ where }) => {
const { userId, teamId, accepted, role } = where;
const mockData = [
@@ -72,6 +72,7 @@ describe("POST /api/event-types", () => {
},
});
req.userId = memberUserId;
// @ts-expect-error - Return type is wrong
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false);
await handler(req, res);
@@ -114,6 +115,7 @@ describe("POST /api/event-types", () => {
req.isSystemWideAdmin = true;
req.userId = adminUserId;
// @ts-expect-error - Return type is wrong
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false);
await handler(req, res);
@@ -138,6 +140,7 @@ describe("POST /api/event-types", () => {
req.isSystemWideAdmin = true;
req.userId = adminUserId;
// @ts-expect-error - Return type is wrong
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => true);
vi.mocked(checkParentEventOwnership).mockImplementationOnce(async () => undefined);
vi.mocked(checkUserMembership).mockImplementationOnce(async () => undefined);
@@ -5,6 +5,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ILicenseKeyService } from "@calcom/ee/common/server/LicenseKeyService";
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
import prisma from "@calcom/prisma";
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
@@ -21,7 +22,7 @@ afterEach(() => {
});
describe("Verify API key", () => {
let service: LicenseKeyService;
let service: ILicenseKeyService;
beforeEach(async () => {
service = await LicenseKeyService.create();
@@ -79,6 +79,15 @@ describe("POST /api/selected-calendars", () => {
integration: "google",
externalId: "ext123",
userId: 444444,
id: "xxx-xxx",
eventTypeId: null,
domainWideDelegationCredentialId: null,
googleChannelId: null,
googleChannelKind: null,
googleChannelResourceId: null,
googleChannelResourceUri: null,
googleChannelExpiration: null,
error: null,
});
await handler(req, res);
@@ -104,10 +113,19 @@ describe("POST /api/selected-calendars", () => {
req.userId = 333333;
prismaMock.selectedCalendar.create.mockResolvedValue({
id: "xxx-xxx",
credentialId: 1,
integration: "google",
externalId: "ext123",
userId: 333333,
googleChannelId: null,
googleChannelKind: null,
googleChannelResourceId: null,
googleChannelResourceUri: null,
googleChannelExpiration: null,
domainWideDelegationCredentialId: null,
eventTypeId: null,
error: null,
});
await handler(req, res);
@@ -2,6 +2,8 @@ import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it, beforeEach } from "vitest";
import { WatchlistSeverity } from "@calcom/prisma/enums";
import { isLockedOrBlocked } from "../../../lib/utils/isLockedOrBlocked";
describe("isLockedOrBlocked", () => {
@@ -16,6 +18,7 @@ describe("isLockedOrBlocked", () => {
{
type: "DOMAIN",
value: "blocked.com",
severity: WatchlistSeverity.CRITICAL,
createdById: 1,
},
],
+2 -1
View File
@@ -1,4 +1,4 @@
FROM node:18-alpine3.20 as build
FROM node:18-alpine as build
ARG DATABASE_DIRECT_URL
ARG DATABASE_URL
@@ -6,6 +6,7 @@ ARG DATABASE_URL
WORKDIR /calcom
RUN set -eux;
RUN ln -s /usr/lib/libssl.so.3 /lib/libssl.so.3
ENV NODE_ENV="production"
ENV NODE_OPTIONS="--max-old-space-size=8192"
+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.70",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.78",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
+2
View File
@@ -30,6 +30,8 @@ const loadConfig = (): AppConfig => {
stripe: {
apiKey: getEnv("STRIPE_API_KEY"),
webhookSecret: getEnv("STRIPE_WEBHOOK_SECRET"),
teamMonthlyPriceId: getEnv("STRIPE_TEAM_MONTHLY_PRICE_ID", "set-team-monthly-price-in-your-env"),
isTeamBillingEnabled: getEnv("IS_TEAM_BILLING_ENABLED", true),
},
app: {
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
+2
View File
@@ -21,6 +21,8 @@ export type AppConfig = {
stripe: {
apiKey: string;
webhookSecret: string;
teamMonthlyPriceId: string;
isTeamBillingEnabled: boolean;
};
app: {
baseUrl: string;
@@ -1,3 +1,4 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Transform, Type } from "class-transformer";
import {
IsBoolean,
@@ -13,53 +14,66 @@ import {
class Location {
@IsString()
@ApiProperty()
optionValue!: string;
@IsString()
@ApiProperty()
value!: string;
}
class Response {
@IsString()
@ApiProperty()
name!: string;
@IsEmail()
@ApiProperty()
email!: string;
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: [String] })
guests!: string[];
@IsOptional()
@ValidateNested()
@Type(() => Location)
@ApiPropertyOptional({ type: Location })
location?: Location;
@IsOptional()
@IsString()
@ApiPropertyOptional()
notes?: string;
}
export class CreateBookingInput_2024_04_15 {
@IsString()
@IsOptional()
@ApiPropertyOptional()
end?: string;
@IsString()
@ApiProperty()
start!: string;
@IsNumber()
@ApiProperty()
eventTypeId!: number;
@IsString()
@IsOptional()
@ApiPropertyOptional()
eventTypeSlug?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
rescheduleUid?: string;
@IsTimeZone()
@ApiProperty()
timeZone!: string;
@Transform(({ value }: { value: string | string[] }) => {
@@ -68,38 +82,48 @@ export class CreateBookingInput_2024_04_15 {
@IsOptional()
@IsArray()
@IsString({ each: true })
@ApiPropertyOptional({ type: [String] })
user?: string[];
@IsString()
@ApiProperty()
language!: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
bookingUid?: string;
@IsObject()
@ApiProperty({ type: Object })
metadata!: Record<string, string>;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
hasHashedBookingLink?: boolean;
@IsString()
@IsOptional()
@ApiPropertyOptional()
hashedLink!: string | null;
@IsString()
@IsOptional()
@ApiPropertyOptional()
seatReferenceUid?: string;
@Type(() => Response)
@ApiProperty({ type: Response })
responses!: Response;
@IsString()
@IsOptional()
@ApiPropertyOptional()
orgSlug?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
locationUrl?: string;
}
@@ -1,4 +1,5 @@
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsBoolean, IsString, IsNumber, IsOptional } from "class-validator";
import type { AppsStatus } from "@calcom/platform-libraries";
@@ -6,23 +7,29 @@ import type { AppsStatus } from "@calcom/platform-libraries";
export class CreateRecurringBookingInput_2024_04_15 extends CreateBookingInput_2024_04_15 {
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
noEmail?: boolean;
@IsOptional()
@IsNumber()
@ApiPropertyOptional()
recurringCount?: number;
@IsOptional()
@ApiPropertyOptional({ type: [Object] })
appsStatus?: AppsStatus[] | undefined;
@IsOptional()
@ApiPropertyOptional({ type: [Object] })
allRecurringDates?: Record<string, string>[];
@IsOptional()
@IsNumber()
@ApiPropertyOptional()
currentRecurringIndex?: number;
@IsString()
@IsOptional()
@ApiPropertyOptional()
recurringEventId?: string;
}
@@ -1,22 +1,27 @@
import { ApiPropertyOptional, ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsOptional, IsArray, IsEmail, IsBoolean, ValidateNested } from "class-validator";
class Attendee {
@IsEmail()
@ApiProperty()
email!: string;
@IsBoolean()
@ApiProperty()
noShow!: boolean;
}
export class MarkNoShowInput_2024_04_15 {
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
noShowHost?: boolean;
@ValidateNested()
@Type(() => Attendee)
@IsArray()
@IsOptional()
@ApiPropertyOptional({ type: [Attendee] })
attendees?: Attendee[];
}
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsString,
@@ -17,145 +17,185 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Metadata {
@IsUrl()
@ApiProperty()
videoCallUrl!: string;
}
class Location {
@IsString()
@ApiProperty()
optionValue!: string;
@IsString()
@ApiProperty()
value!: string;
}
class Response {
@IsString()
@ApiProperty()
name!: string;
@IsEmail()
@ApiProperty()
email!: string;
@IsString()
@ApiProperty()
notes!: string;
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: [String] })
guests!: string[];
@ValidateNested()
@Type(() => Location)
@ApiProperty({ type: Location })
location!: Location;
}
class User {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty({ type: String, nullable: true })
name!: string | null;
@IsEmail()
@ApiProperty()
email!: string;
@IsString()
@ApiProperty({ type: String, nullable: true })
username!: string | null;
@IsString()
@ApiProperty()
timeZone!: string;
}
class Attendee {
@IsString()
@ApiProperty()
name!: string;
@IsEmail()
@ApiProperty()
email!: string;
@IsString()
@ApiProperty()
timeZone!: string;
}
class EventType {
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
eventName!: string | null;
@IsString()
@ApiProperty()
slug!: string;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
timeZone!: string | null;
}
class GetBookingData_2024_04_15 {
@IsString()
@ApiProperty()
title!: string;
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty()
uid!: string;
@IsString()
@ApiProperty({ type: String, nullable: true })
description!: string | null;
@IsObject()
@ApiProperty({ type: Object })
customInputs!: any;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
smsReminderNumber!: string | null;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
recurringEventId!: string | null;
@IsDateString()
@ApiProperty()
startTime!: Date;
@IsDateString()
@ApiProperty()
endTime!: Date;
@IsUrl()
@ApiProperty({ type: String, nullable: true })
location!: string | null;
@IsString()
@ApiProperty()
status!: string;
@Type(() => Metadata)
@ApiProperty({ type: Metadata })
metadata!: Metadata | any;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
cancellationReason!: string | null;
@ValidateNested()
@Type(() => Response)
@ApiProperty({ type: Response })
responses!: Response | any;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
rejectionReason!: string | null;
@IsString()
@IsEmail()
@ApiProperty({ type: String, nullable: true })
userPrimaryEmail!: string | null;
@ValidateNested()
@Type(() => User)
@ApiProperty({ type: User, nullable: true })
user!: User | null;
@ValidateNested()
@Type(() => Attendee)
@IsArray()
@ApiProperty({ type: [Attendee] })
attendees!: Attendee[];
@IsInt()
@ApiProperty({ type: Number, nullable: true })
eventTypeId!: number | null;
@ValidateNested()
@Type(() => EventType)
@ApiProperty({ type: EventType, nullable: true })
eventType!: EventType | null;
}
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsString,
@@ -26,179 +26,230 @@ enum Status {
class Attendee {
@IsInt()
@ApiProperty()
id!: number;
@IsEmail()
@ApiProperty()
email!: string;
@IsString()
@ApiProperty()
name!: string;
@IsString()
@ApiProperty()
timeZone!: string;
@IsString()
@ApiProperty({ type: String, nullable: true })
locale!: string | null;
@IsInt()
@ApiProperty({ type: Number, nullable: true })
bookingId!: number | null;
}
class EventType {
@IsString()
@IsOptional()
@IsString()
@ApiPropertyOptional()
slug?: string;
@IsOptional()
@IsInt()
@ApiPropertyOptional()
id?: number;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
eventName?: string | null;
@IsInt()
@ApiProperty()
price!: number;
@IsOptional()
@ApiPropertyOptional()
recurringEvent?: any;
@IsString()
@ApiProperty()
currency!: string;
@IsObject()
@ApiProperty()
metadata!: any;
@IsBoolean()
@IsOptional()
seatsShowAttendees?: boolean | undefined | null;
@ApiPropertyOptional({ type: Boolean, nullable: true })
seatsShowAttendees?: boolean | null;
@IsBoolean()
@IsOptional()
seatsShowAvailabilityCount?: boolean | undefined | null;
@ApiPropertyOptional({ type: Boolean, nullable: true })
seatsShowAvailabilityCount?: boolean | null;
@IsOptional()
@ApiPropertyOptional({ nullable: true })
team?: any | null;
}
class Reference {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty()
type!: string;
@IsString()
@ApiProperty()
uid!: string;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
meetingId?: string | null;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
thirdPartyRecurringEventId?: string | null;
@IsString()
@ApiProperty({ type: String, nullable: true })
meetingPassword!: string | null;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
meetingUrl?: string | null;
@IsInt()
@ApiProperty({ type: Number, nullable: true })
bookingId!: number | null;
@IsEmail()
@ApiProperty({ type: String, nullable: true })
externalCalendarId!: string | null;
@IsOptional()
@ApiPropertyOptional()
deleted?: any;
@IsInt()
@ApiProperty({ type: Number, nullable: true })
credentialId!: number | null;
}
class User {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty({ type: String, nullable: true })
name!: string | null;
@IsEmail()
@ApiProperty()
email!: string;
}
class GetBookingsDataEntry {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty()
title!: string;
@IsOptional()
@IsEmail()
@ApiProperty({ type: String, nullable: true })
userPrimaryEmail?: string | null;
@IsString()
@ApiProperty({ type: String, nullable: true })
description!: string | null;
@IsObject()
@ApiProperty({ type: Object })
customInputs!: object | any;
@IsDateString()
@ApiProperty()
startTime!: string;
@IsDateString()
@ApiProperty()
endTime!: string;
@ValidateNested({ each: true })
@Type(() => Attendee)
@IsArray()
@ApiProperty({ type: [Attendee] })
attendees!: Attendee[];
@ApiProperty()
metadata!: any;
@IsString()
@ApiProperty()
uid!: string;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
recurringEventId!: string | null;
@IsUrl()
@ApiProperty({ type: String, nullable: true })
location!: string | null;
@ValidateNested()
@Type(() => EventType)
@ApiProperty({ type: EventType })
eventType!: EventType;
@IsEnum(Status)
status!: "CANCELLED" | "REJECTED" | "ACCEPTED" | "PENDING" | "AWAITING_HOST";
@ApiProperty({ enum: Status, type: String })
status!: Status;
@IsBoolean()
@ApiProperty({ type: Boolean })
paid!: boolean;
@IsArray()
@ApiProperty()
payment!: any[];
@ValidateNested()
@Type(() => Reference)
@IsArray()
@ApiProperty({ type: [Reference] })
references!: Reference[];
@IsBoolean()
@ApiProperty({ type: Boolean })
isRecorded!: boolean;
@IsArray()
@ApiProperty()
seatsReferences!: any[];
@ValidateNested()
@Type(() => User)
@ApiProperty({ type: User })
user!: User | null;
@IsOptional()
@ApiPropertyOptional()
rescheduled?: any;
}
@@ -206,17 +257,20 @@ class GetBookingsData_2024_04_15 {
@ValidateNested()
@Type(() => GetBookingsDataEntry)
@IsArray()
@ApiProperty({ type: [GetBookingsDataEntry] })
bookings!: GetBookingsDataEntry[];
@IsArray()
@ApiProperty()
recurringInfo!: any[];
@IsInt()
@ApiProperty({ type: Number, nullable: true })
nextCursor!: number | null;
}
export class GetBookingsOutput_2024_04_15 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS], type: String })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsString, IsEnum, IsOptional, ValidateNested, IsArray, IsEmail, IsBoolean } from "class-validator";
@@ -6,24 +6,29 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Attendee {
@IsEmail()
@ApiProperty()
email!: string;
@IsBoolean()
@ApiProperty()
noShow!: boolean;
}
class HandleMarkNoShowData_2024_04_15 {
@IsString()
@ApiProperty()
message!: string;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
noShowHost?: boolean;
@ValidateNested()
@Type(() => Attendee)
@IsArray()
@IsOptional()
@ApiPropertyOptional({ type: [Attendee] })
attendees?: Attendee[];
}
@@ -169,6 +169,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.eventTypeId).toEqual(eventTypeId);
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -181,6 +181,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -201,6 +202,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.recurringBookingUid).toBeDefined();
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -256,6 +258,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -277,6 +280,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.recurringBookingUid).toBeDefined();
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -296,6 +300,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(thirdBooking.recurringBookingUid).toBeDefined();
expect(thirdBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -205,6 +205,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -278,6 +279,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
const firstAttendee = data.attendees.find((attendee) => attendee.name === nameAttendeeOne);
expect(firstAttendee).toEqual({
name: createdSeatedBooking.attendees[0].name,
email: createdSeatedBooking.attendees[0].email,
timeZone: createdSeatedBooking.attendees[0].timeZone,
language: createdSeatedBooking.attendees[0].language,
absent: false,
@@ -291,6 +293,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
const secondAttendee = data.attendees.find((attendee) => attendee.name === nameAttendeeTwo);
expect(secondAttendee).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -368,6 +371,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.attendees.length).toEqual(1);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -402,6 +406,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.attendees.length).toEqual(1);
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -576,6 +581,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
const attendee = createdSeatedBooking.attendees.find((a) => a.seatUid === body.seatUid);
expect(data.attendees[0]).toEqual({
name: attendee?.name,
email: attendee?.email,
timeZone: attendee?.timeZone,
language: attendee?.language,
absent: false,
@@ -835,6 +841,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.attendees.length).toEqual(1);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -869,6 +876,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.attendees.length).toEqual(1);
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -946,6 +954,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.attendees.length).toEqual(2);
expect(firstBooking.attendees[0]).toEqual({
name: nameAttendeeOne,
email: emailAttendeeOne,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -958,6 +967,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
expect(firstBooking.attendees[1]).toEqual({
name: nameAttendeeTwo,
email: emailAttendeeTwo,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -992,6 +1002,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.attendees.length).toEqual(2);
expect(secondBooking.attendees[0]).toEqual({
name: nameAttendeeOne,
email: emailAttendeeOne,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -1004,6 +1015,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
expect(secondBooking.attendees[1]).toEqual({
name: nameAttendeeTwo,
email: emailAttendeeTwo,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -307,6 +307,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -359,12 +360,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees.length).toEqual(2);
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(data.attendees[1]).toEqual({
name: teamUser2.name,
email: teamUser2.email,
timeZone: teamUser2.timeZone,
language: teamUser2.locale,
absent: false,
@@ -364,6 +364,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -434,6 +435,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -455,6 +457,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.recurringBookingUid).toBeDefined();
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -475,6 +478,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(thirdBooking.recurringBookingUid).toBeDefined();
expect(thirdBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -340,7 +340,10 @@ export class BookingsService_2024_08_13 {
}
const bookingRequest = await this.inputService.createCancelBookingRequest(request, bookingUid, body);
await handleCancelBooking(bookingRequest);
const res = await handleCancelBooking(bookingRequest);
if (!res.onlyRemovedAttendee) {
await this.billingService.cancelUsageByBookingUid(res.bookingUid);
}
if ("cancelSubsequentBookings" in body && body.cancelSubsequentBookings) {
return this.getAllRecurringBookingsByIndividualUid(bookingUid);
@@ -1,7 +1,7 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import {
bookingResponsesSchema,
seatedBookingResponsesSchema,
seatedBookingDataSchema,
} from "@/ee/bookings/2024-08-13/services/output.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
@@ -340,7 +340,7 @@ export class InputBookingsService_2024_08_13 {
throw new NotFoundException(`Seat with uid=${inputBooking.seatUid} does not exist.`);
}
const { responses: bookingResponses } = seatedBookingResponsesSchema.parse(seat.data);
const { responses: bookingResponses } = seatedBookingDataSchema.parse(seat.data);
const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
if (!attendee) {
@@ -1,4 +1,11 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import {
defaultBookingMetadata,
defaultBookingResponses,
defaultSeatedBookingData,
defaultSeatedBookingMetadata,
} from "@/lib/safe-parse/default-responses-booking";
import { safeParse } from "@/lib/safe-parse/safe-parse";
import { Injectable } from "@nestjs/common";
import { plainToClass } from "class-transformer";
import { DateTime } from "luxon";
@@ -30,9 +37,10 @@ export const bookingResponsesSchema = z
guests: z.array(z.string()).optional(),
rescheduleReason: z.string().optional(),
})
.passthrough();
.passthrough()
.describe("BookingResponses");
export const seatedBookingResponsesSchema = z
export const seatedBookingDataSchema = z
.object({
responses: z
.object({
@@ -47,7 +55,10 @@ export const seatedBookingResponsesSchema = z
})
.passthrough(),
})
.passthrough();
.passthrough()
.describe("SeatedBookingData");
const seatedBookingMetadataSchema = z.object({}).catchall(z.string()).describe("SeatedBookingMetadata");
type DatabaseUser = { id: number; name: string | null; email: string; username: string | null };
@@ -72,8 +83,6 @@ type BookingWithUser = Booking & { user: DatabaseUser | null };
type DatabaseMetadata = z.infer<typeof bookingMetadataSchema>;
const seatedBookingMetadataSchema = z.object({}).catchall(z.string());
@Injectable()
export class OutputBookingsService_2024_08_13 {
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
@@ -82,8 +91,12 @@ 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 = bookingMetadataSchema.parse(databaseBooking.metadata);
const bookingResponses = safeParse(
bookingResponsesSchema,
databaseBooking.responses,
defaultBookingResponses
);
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const booking = {
@@ -167,8 +180,12 @@ 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 = bookingMetadataSchema.parse(databaseBooking.metadata);
const bookingResponses = safeParse(
bookingResponsesSchema,
databaseBooking.responses,
defaultBookingResponses
);
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const booking = {
@@ -225,7 +242,7 @@ 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 metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const booking = {
@@ -254,7 +271,11 @@ export class OutputBookingsService_2024_08_13 {
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
parsed.attendees = databaseBooking.attendees.map((attendee) => {
const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data);
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData
);
const attendeeData = {
name: attendee.name,
@@ -267,7 +288,11 @@ export class OutputBookingsService_2024_08_13 {
};
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
attendeeParsed.bookingFieldsResponses = responses || {};
attendeeParsed.metadata = seatedBookingMetadataSchema.parse(attendee.bookingSeat?.metadata);
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
@@ -320,7 +345,7 @@ 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 metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const booking = {
@@ -353,7 +378,11 @@ export class OutputBookingsService_2024_08_13 {
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
parsed.attendees = databaseBooking.attendees.map((attendee) => {
const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data);
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData
);
const attendeeData = {
name: attendee.name,
@@ -366,7 +395,11 @@ export class OutputBookingsService_2024_08_13 {
};
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
attendeeParsed.bookingFieldsResponses = responses || {};
attendeeParsed.metadata = seatedBookingMetadataSchema.parse(attendee.bookingSeat?.metadata);
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
return attendeeParsed;
@@ -86,18 +86,16 @@ export class CalendarsController {
@UseGuards(ApiAuthGuard)
@Get("/busy-times")
@ApiOperation({ summary: "Get busy times" })
@ApiOperation({
summary: "Get busy times",
description:
"Get busy times from a calendar. Example request URL is `https://api.cal.com/v2/calendars/busy-times?loggedInUsersTz=Europe%2FMadrid&dateFrom=2024-12-18&dateTo=2024-12-18&calendarsToLoad[0][credentialId]=135&calendarsToLoad[0][externalId]=skrauciz%40gmail.com`",
})
async getBusyTimes(
@Query() queryParams: CalendarBusyTimesInput,
@GetUser() user: UserWithProfile
): Promise<GetBusyTimesOutput> {
const { loggedInUsersTz, dateFrom, dateTo, calendarsToLoad } = queryParams;
if (!dateFrom || !dateTo) {
return {
status: SUCCESS_STATUS,
data: [],
};
}
const busyTimes = await this.calendarsService.getBusyTimes(
calendarsToLoad,
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import {
ArrayNotEmpty,
IsBoolean,
@@ -50,11 +50,10 @@ export class CreateIcsFeedInputDto {
urls!: string[];
@IsBoolean()
@ApiProperty({
@ApiPropertyOptional({
example: false,
description: "Whether to allowing writing to the calendar or not",
type: "boolean",
required: false,
default: true,
})
@IsOptional()
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsDate, IsEnum, IsOptional, IsString, ValidateNested } from "class-validator";
@@ -6,13 +6,16 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class BusyTimesOutput {
@IsDate()
@ApiProperty({ type: Date })
start!: Date;
@IsDate()
@ApiProperty({ type: Date })
end!: Date;
@IsOptional()
@IsString()
@ApiPropertyOptional({ type: String, nullable: true })
source?: string | null;
}
@@ -24,5 +27,6 @@ export class GetBusyTimesOutput {
@ValidateNested()
@Type(() => BusyTimesOutput)
@IsArray()
@ApiProperty({ type: [BusyTimesOutput] })
data!: BusyTimesOutput[];
}
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsArray,
@@ -18,195 +18,246 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Integration {
@IsOptional()
@IsObject()
@ApiPropertyOptional({ type: Object, nullable: true })
appData?: object | null;
@IsOptional()
@IsString()
@ApiPropertyOptional()
dirName?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
__template?: string;
@IsString()
@ApiProperty()
name!: string;
@IsString()
@ApiProperty()
description!: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
installed?: boolean;
@IsString()
@ApiProperty()
type!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
title?: string;
@IsString()
@ApiProperty()
variant!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
category?: string;
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: [String] })
categories!: string[];
@IsString()
@ApiProperty()
logo!: string;
@IsString()
@ApiProperty()
publisher!: string;
@IsString()
@ApiProperty()
slug!: string;
@IsUrl()
@ApiProperty()
url!: string;
@IsEmail()
@ApiProperty()
email!: string;
@IsOptional()
@IsObject()
@ApiProperty({ type: Object, nullable: true })
locationOption!: object | null;
}
class Primary {
@IsEmail()
@ApiProperty()
externalId!: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
integration?: string;
@IsOptional()
@IsEmail()
@ApiPropertyOptional()
name?: string;
@IsBoolean()
@ApiProperty({ type: Boolean, nullable: true })
primary!: boolean | null;
@IsBoolean()
@ApiProperty()
readOnly!: boolean;
@IsEmail()
@IsOptional()
@ApiPropertyOptional()
email?: string;
@IsBoolean()
@ApiProperty()
isSelected!: boolean;
@IsInt()
@ApiProperty()
credentialId!: number;
}
export class Calendar {
@IsEmail()
@ApiProperty()
externalId!: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
integration?: string;
@IsEmail()
@IsOptional()
@ApiPropertyOptional()
name?: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ type: Boolean, nullable: true })
primary?: boolean | null;
@IsBoolean()
@ApiProperty()
readOnly!: boolean;
@IsEmail()
@IsOptional()
@ApiPropertyOptional()
email?: string;
@IsBoolean()
@ApiProperty()
isSelected!: boolean;
@IsInt()
@ApiProperty()
credentialId!: number;
}
export class ConnectedCalendar {
@ValidateNested()
@IsObject()
@ApiProperty({ type: Integration })
integration!: Integration;
@IsInt()
@ApiProperty()
credentialId!: number;
@ValidateNested()
@IsObject()
@IsOptional()
@ApiPropertyOptional({ type: Primary })
primary?: Primary;
@ValidateNested({ each: true })
@IsArray()
@IsOptional()
@ApiPropertyOptional({ type: [Calendar] })
calendars?: Calendar[];
}
class DestinationCalendar {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty()
integration!: string;
@IsEmail()
@ApiProperty()
externalId!: string;
@IsEmail()
@ApiProperty({ type: String, nullable: true })
primaryEmail!: string | null;
@IsInt()
@ApiProperty({ nullable: true })
userId!: number | null;
@IsOptional()
@IsInt()
@ApiProperty({ type: Number, nullable: true })
eventTypeId!: number | null;
@IsInt()
@ApiProperty({ type: Number, nullable: true })
credentialId!: number | null;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
name?: string | null;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
primary?: boolean;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
readOnly?: boolean;
@IsEmail()
@IsOptional()
@ApiPropertyOptional()
email?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
integrationTitle?: string;
}
export class ConnectedCalendarsData {
@ValidateNested({ each: true })
@IsArray()
@ApiProperty({ type: [ConnectedCalendar] })
connectedCalendars!: ConnectedCalendar[];
@ValidateNested()
@IsObject()
@ApiProperty({ type: DestinationCalendar })
destinationCalendar!: DestinationCalendar;
}
export class ConnectedCalendarsOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
@@ -214,5 +265,6 @@ export class ConnectedCalendarsOutput {
@ValidateNested()
@Type(() => ConnectedCalendarsData)
@ApiProperty({ type: ConnectedCalendarsData })
data!: ConnectedCalendarsData;
}
@@ -19,7 +19,10 @@ import { DateTime } from "luxon";
import { z } from "zod";
import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries";
import {
getConnectedDestinationCalendarsAndEnsureDefaultsInDb,
getBusyCalendarTimes,
} from "@calcom/platform-libraries";
import { Calendar } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
@@ -41,12 +44,18 @@ export class CalendarsService {
if (!userWithCalendars) {
throw new NotFoundException("User not found");
}
return getConnectedDestinationCalendars(
userWithCalendars,
false,
this.dbWrite.prisma as unknown as PrismaClient
);
return getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
user: {
...userWithCalendars,
allSelectedCalendars: userWithCalendars.selectedCalendars,
userLevelSelectedCalendars: userWithCalendars.selectedCalendars.filter(
(calendar) => !calendar.eventTypeId
),
},
onboarding: false,
eventTypeId: null,
prisma: this.dbWrite.prisma as unknown as PrismaClient,
});
}
async getBusyTimes(
@@ -64,7 +73,6 @@ export class CalendarsService {
);
try {
const calendarBusyTimes = await getBusyCalendarTimes(
"",
credentials,
dateFrom,
dateTo,
@@ -116,7 +116,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
) {
// User chose not to authorize your app or didn't authorize your app
// redirect directly without oauth code
if (!code) {
if (!code || code === "undefined") {
return { url: redir || origin };
}
@@ -151,7 +151,7 @@ export class OutlookService implements OAuthCalendarApp {
redir?: string
) {
// if code is not defined, user denied to authorize office 365 app, just redirect straight away
if (!code) {
if (!code || code === "undefined") {
return { url: redir || origin };
}
@@ -1,5 +1,5 @@
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
import { ApiProperty as DocsProperty, ApiPropertyOptional, ApiHideProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsString,
@@ -36,7 +36,10 @@ export class CreateEventTypeInput_2024_04_15 {
@IsOptional()
@IsString()
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
@ApiPropertyOptional({
type: String,
example: CREATE_EVENT_DESCRIPTION_EXAMPLE,
})
description?: string;
@IsOptional()
@@ -48,30 +51,38 @@ export class CreateEventTypeInput_2024_04_15 {
@ValidateNested({ each: true })
@Type(() => EventTypeLocation_2024_04_15)
@IsArray()
@ApiPropertyOptional({
type: [EventTypeLocation_2024_04_15],
})
locations?: EventTypeLocation_2024_04_15[];
@IsBoolean()
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
disableGuests?: boolean;
@IsOptional()
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
slotInterval?: number;
@IsOptional()
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
minimumBookingNotice?: number;
@IsOptional()
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
beforeEventBuffer?: number;
@IsOptional()
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
afterEventBuffer?: number;
// @ApiHideProperty()
@@ -1,4 +1,4 @@
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
import { ApiProperty, ApiHideProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validator";
// note(Lauris): We will gradually expose more properties if any customer needs them.
@@ -6,7 +6,7 @@ import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validato
export class EventTypeLocation_2024_04_15 {
@IsString()
@DocsProperty({ example: "link" })
@ApiProperty({ example: "link" })
type!: string;
@IsOptional()
@@ -16,7 +16,7 @@ export class EventTypeLocation_2024_04_15 {
@IsOptional()
@IsUrl()
@DocsProperty({ example: "https://masterchief.com/argentina/flan/video/9129412" })
@ApiPropertyOptional({ example: "https://masterchief.com/argentina/flan/video/9129412" })
link?: string;
@IsOptional()
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsBoolean, IsOptional, IsString } from "class-validator";
@@ -6,11 +6,11 @@ export class GetPublicEventTypeQueryParams_2024_04_15 {
@Transform(({ value }: { value: string }) => value === "true")
@IsBoolean()
@IsOptional()
@ApiProperty({ required: false })
@ApiPropertyOptional()
isTeamEvent?: boolean;
@ApiProperty({ required: false })
@IsString()
@IsOptional()
@IsString()
@ApiPropertyOptional({ type: String, nullable: true })
org?: string | null;
}
@@ -2,6 +2,7 @@ import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/e
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
import { Frequency } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/frequency";
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsString,
@@ -21,206 +22,259 @@ import {
class Option {
@IsString()
@ApiProperty()
value!: string;
@IsString()
@ApiProperty()
label!: string;
}
class Source {
@IsString()
@ApiProperty()
id!: string;
@IsString()
@ApiProperty()
type!: string;
@IsString()
@ApiProperty()
label!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
editUrl?: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
fieldRequired?: boolean;
}
class View {
@IsString()
@ApiProperty()
id!: string;
@IsString()
@ApiProperty()
label!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
description?: string;
}
class OptionsInput {
@IsString()
@ApiProperty({
description: 'Type of the field, can be one of "address", "text", or "phone".',
enum: ["address", "text", "phone"],
example: "text",
})
type!: "address" | "text" | "phone";
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
required?: boolean;
@IsOptional()
@IsString()
@ApiPropertyOptional()
placeholder?: string;
}
class VariantField {
@IsString()
@ApiProperty()
type!: BaseField;
@IsString()
@ApiProperty()
name!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
label?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
labelAsSafeHtml?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
placeholder?: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
required?: boolean;
}
class Variant {
@ValidateNested({ each: true })
@Type(() => VariantField)
@ApiProperty({ type: [VariantField] })
fields!: VariantField[];
}
class VariantsConfig {
@ApiProperty({ type: Object })
variants!: Record<string, Variant>;
}
export class BookingField_2024_04_15 {
@IsEnum(BaseField)
@ApiProperty({ enum: BaseField })
type!: BaseField;
@IsString()
@ApiProperty()
name!: string;
@IsOptional()
@ValidateNested({ each: true })
@Type(() => Option)
@ApiPropertyOptional({ type: [Option] })
options?: Option[];
@IsOptional()
@IsString()
@ApiPropertyOptional()
label?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
labelAsSafeHtml?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
defaultLabel?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
placeholder?: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
required?: boolean;
@IsOptional()
@IsString()
@ApiPropertyOptional()
getOptionsAt?: string;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => OptionsInput)
@ApiPropertyOptional()
optionsInputs?: Record<string, OptionsInput>;
@IsOptional()
@IsString()
@ApiPropertyOptional()
variant?: string;
@IsOptional()
@ValidateNested()
@Type(() => VariantsConfig)
@ApiPropertyOptional({ type: VariantsConfig })
variantsConfig?: VariantsConfig;
@IsOptional()
@ValidateNested({ each: true })
@Type(() => View)
@ApiPropertyOptional({ type: [View] })
views?: View[];
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
hideWhenJustOneOption?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
hidden?: boolean;
@IsOptional()
@IsEnum(Editable)
@ApiPropertyOptional()
editable?: Editable;
@IsOptional()
@ValidateNested({ each: true })
@Type(() => Source)
@ApiPropertyOptional({ type: [Source] })
sources?: Source[];
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
disableOnPrefill?: boolean;
}
export class RecurringEvent_2024_04_15 {
@IsDate()
@IsOptional()
@ApiPropertyOptional({ type: Date })
dtstart?: Date;
@IsInt()
@ApiProperty()
interval!: number;
@IsInt()
@ApiProperty()
count!: number;
@IsEnum(Frequency)
@ApiProperty({ enum: Frequency })
freq!: Frequency;
@IsDate()
@IsOptional()
@ApiPropertyOptional({ type: Date })
until?: Date;
@IsString()
@IsOptional()
@ApiPropertyOptional()
tzid?: string;
}
export class IntervalLimits_2024_04_15 {
@IsNumber()
@IsOptional()
@ApiPropertyOptional()
PER_DAY?: number;
@IsNumber()
@IsOptional()
@ApiPropertyOptional()
PER_WEEK?: number;
@IsNumber()
@IsOptional()
@ApiPropertyOptional()
PER_MONTH?: number;
@IsNumber()
@IsOptional()
@ApiPropertyOptional()
PER_YEAR?: number;
}
@@ -228,27 +282,33 @@ export class UpdateEventTypeInput_2024_04_15 {
@IsInt()
@Min(1)
@IsOptional()
@ApiPropertyOptional()
length?: number;
@IsString()
@IsOptional()
@ApiPropertyOptional()
slug?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
title?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
description?: string;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
hidden?: boolean;
@ValidateNested({ each: true })
@Type(() => EventTypeLocation_2024_04_15)
@IsOptional()
@ApiPropertyOptional({ type: [EventTypeLocation_2024_04_15] })
locations?: EventTypeLocation_2024_04_15[];
// @IsInt()
@@ -283,6 +343,7 @@ export class UpdateEventTypeInput_2024_04_15 {
@IsArray()
@ValidateNested({ each: true })
@Type(() => BookingField_2024_04_15)
@ApiPropertyOptional({ type: [BookingField_2024_04_15] })
bookingFields?: BookingField_2024_04_15[];
// @IsString()
@@ -328,6 +389,7 @@ export class UpdateEventTypeInput_2024_04_15 {
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
disableGuests?: boolean;
// @IsBoolean()
@@ -337,16 +399,19 @@ export class UpdateEventTypeInput_2024_04_15 {
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
minimumBookingNotice?: number;
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
beforeEventBuffer?: number;
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
afterEventBuffer?: number;
// @IsInt()
@@ -384,6 +449,7 @@ export class UpdateEventTypeInput_2024_04_15 {
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
slotInterval?: number;
// @IsString()
@@ -12,7 +12,7 @@ import {
IntervalLimits_2024_04_15,
RecurringEvent_2024_04_15,
} from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
import { ApiHideProperty, ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsArray,
@@ -29,23 +29,23 @@ import {
export class EventTypeOutput {
@IsInt()
@DocsProperty({ example: 1 })
@ApiProperty({ example: 1 })
id!: number;
@IsInt()
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
@ApiProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
length!: number;
@IsString()
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
@ApiProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
slug!: string;
@IsString()
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
@ApiProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
title!: string;
@IsString()
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
@ApiProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE, nullable: true })
description!: string | null;
@IsBoolean()
@@ -55,6 +55,7 @@ export class EventTypeOutput {
@ValidateNested({ each: true })
@Type(() => EventTypeLocation_2024_04_15)
@IsArray()
@ApiProperty({ type: [EventTypeLocation_2024_04_15], nullable: true })
locations!: EventTypeLocation_2024_04_15[] | null;
@IsInt()
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsBoolean,
@@ -17,330 +17,421 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Location {
@IsString()
@ApiProperty()
type!: string;
}
class Source {
@IsString()
@ApiProperty()
id!: string;
@IsString()
@ApiProperty()
type!: string;
@IsString()
@ApiProperty()
label!: string;
}
class OptionInput {
@IsString()
@ApiProperty()
type!: string;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
required?: boolean;
@IsString()
@IsOptional()
@ApiPropertyOptional()
placeholder?: string;
}
class BookingField {
@IsString()
@ApiProperty()
name!: string;
@IsString()
@ApiProperty()
type!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
defaultLabel?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
label?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
placeholder?: string;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
required?: boolean;
@IsOptional()
@ApiPropertyOptional()
getOptionsAt?: string;
@IsObject()
@IsOptional()
@ApiPropertyOptional()
optionsInputs?: { [key: string]: OptionInput };
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
hideWhenJustOneOption?: boolean;
@IsString()
@IsOptional()
@ApiPropertyOptional()
editable?: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => Source)
@IsOptional()
@ApiPropertyOptional({ type: [Source] })
sources?: Source[];
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
disableOnPrefill?: boolean;
}
class Organization {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
slug?: string | null;
@IsString()
@ApiProperty()
name!: string;
@IsOptional()
metadata!: Record<string, any>;
@ApiPropertyOptional({ type: Object })
metadata?: Record<string, any>;
}
class Profile {
@IsString()
@ApiProperty({ type: String, nullable: true })
username!: string | null;
@IsInt()
@ApiProperty({ type: Number, nullable: true })
id!: number | null;
@IsInt()
@IsOptional()
@ApiPropertyOptional()
userId?: number;
@IsString()
@IsOptional()
@ApiPropertyOptional()
uid?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
name?: string;
@IsInt()
@ApiProperty({ type: Number, nullable: true })
organizationId!: number | null;
@ValidateNested()
@Type(() => Organization)
@ApiPropertyOptional({ type: Organization, nullable: true })
organization?: Organization | null;
@IsString()
@ApiProperty()
upId!: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
image?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
brandColor?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
darkBrandColor?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
theme?: string;
@IsOptional()
@ApiPropertyOptional()
bookerLayouts?: any;
}
class Owner {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
avatarUrl?: string | null;
@IsString()
@ApiProperty({ type: String, nullable: true })
username!: string | null;
@IsString()
@ApiProperty({ type: String, nullable: true })
name!: string | null;
@IsString()
@ApiProperty()
weekStart!: string;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
brandColor?: string | null;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
darkBrandColor?: string | null;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
theme?: string | null;
@IsOptional()
metadata!: any;
@ApiPropertyOptional()
metadata?: any;
@IsInt()
@IsOptional()
@ApiPropertyOptional({ type: Number, nullable: true })
defaultScheduleId?: number | null;
@IsString()
@ApiProperty({ type: String, nullable: true })
nonProfileUsername!: string | null;
@ValidateNested()
@Type(() => Profile)
@ApiProperty({ type: Profile })
profile!: Profile;
}
class User {
@IsString()
@ApiProperty({ type: String, nullable: true })
username!: string | null;
@IsString()
@ApiProperty({ type: String, nullable: true })
name!: string | null;
@IsString()
@ApiProperty()
weekStart!: string;
@IsInt()
@IsOptional()
@ApiPropertyOptional()
organizationId?: number;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
avatarUrl?: string | null;
@ValidateNested()
@ApiProperty({ type: Profile })
profile!: Profile;
@IsString()
@ApiProperty()
bookerUrl!: string;
}
class Schedule {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty({ type: String, nullable: true })
timeZone!: string | null;
}
class PublicEventTypeOutput {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty()
title!: string;
@IsString()
@ApiProperty()
description!: string;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
eventName?: string | null;
@IsString()
@ApiProperty()
slug!: string;
@IsBoolean()
@ApiProperty()
isInstantEvent!: boolean;
@IsOptional()
@ApiPropertyOptional()
aiPhoneCallConfig?: any;
@IsOptional()
@ApiPropertyOptional()
schedulingType?: any;
@IsInt()
@ApiProperty()
length!: number;
@IsArray()
@ValidateNested({ each: true })
@Type(() => Location)
@ApiProperty({ type: [Location] })
locations!: Location[];
@IsArray()
@ApiProperty({ type: [Object] })
customInputs!: any[];
@IsBoolean()
@ApiProperty()
disableGuests!: boolean;
@IsObject()
@ApiProperty({ type: Object, nullable: true })
metadata!: object | null;
@IsBoolean()
@ApiProperty()
lockTimeZoneToggleOnBookingPage!: boolean;
@IsBoolean()
@ApiProperty()
requiresConfirmation!: boolean;
@IsBoolean()
@ApiProperty()
requiresBookerEmailVerification!: boolean;
@IsOptional()
@ApiPropertyOptional()
recurringEvent?: any;
@IsNumber()
@ApiProperty()
price!: number;
@IsString()
@ApiProperty()
currency!: string;
@IsOptional()
@ApiPropertyOptional({ type: Number, nullable: true })
seatsPerTimeSlot?: number | null;
@IsBoolean()
@ApiProperty({ type: Boolean, nullable: true })
seatsShowAvailabilityCount!: boolean | null;
@IsArray()
@ValidateNested({ each: true })
@Type(() => BookingField)
@ApiProperty({ type: [BookingField] })
bookingFields!: BookingField[];
@IsOptional()
@ApiPropertyOptional()
team?: any;
@IsOptional()
@IsUrl()
@ApiPropertyOptional({ type: String, nullable: true })
successRedirectUrl?: string | null;
@IsArray()
@ApiProperty()
workflows!: any[];
@IsArray()
@ApiProperty()
hosts!: any[];
@ValidateNested()
@Type(() => Owner)
@ApiProperty({ type: Owner, nullable: true })
owner!: Owner | null;
@ValidateNested()
@Type(() => Schedule)
@ApiProperty({ type: Schedule, nullable: true })
schedule!: Schedule | null;
@IsBoolean()
@ApiProperty()
hidden!: boolean;
@IsBoolean()
@ApiProperty()
assignAllTeamMembers!: boolean;
@IsOptional()
@ApiPropertyOptional()
bookerLayouts?: any;
@IsArray()
@ValidateNested({ each: true })
@Type(() => User)
@ApiProperty({ type: [User] })
users!: User[];
@IsObject()
@ApiProperty({ type: Object })
entity!: object;
@IsBoolean()
@@ -291,6 +291,7 @@ describe("Event types Endpoints", () => {
placeholder: "select language",
options: ["javascript", "python", "cobol"],
disableOnPrefill: true,
hidden: false,
},
],
scheduleId: firstSchedule.id,
@@ -381,10 +382,24 @@ describe("Event types Endpoints", () => {
const requestBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
{ isDefault: true, required: true, slug: "email", type: "email" },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
// note(Lauris): location booking field is added if multiple locations are passed
{ isDefault: true, required: false, slug: "location", type: "radioInput" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
{
isDefault: true,
required: false,
slug: "location",
type: "radioInput",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
required: false,
slug: "rescheduleReason",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
...requestBookingFields
.filter((field) => field.type !== "name" && field.type !== "email")
.map((field) => ({ isDefault: false, ...field })),
@@ -787,6 +802,7 @@ describe("Event types Endpoints", () => {
placeholder: "select language",
options: ["javascript", "python", "cobol"],
disableOnPrefill: false,
hidden: false,
},
],
bookingLimitsCount: {
@@ -853,8 +869,15 @@ describe("Event types Endpoints", () => {
const requestBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
{ isDefault: true, required: true, slug: "email", type: "email" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{
isDefault: true,
required: false,
slug: "rescheduleReason",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
...requestBookingFields
.filter((field) => field.type !== "name" && field.type !== "email")
.map((field) => ({ isDefault: false, ...field })),
@@ -1055,14 +1078,44 @@ describe("Event types Endpoints", () => {
let legacyEventTypeId2: number;
const expectedReturnSystemFields = [
{ isDefault: true, required: true, slug: "name", type: "name" },
{ isDefault: true, required: true, slug: "email", type: "email" },
{ isDefault: true, type: "radioInput", slug: "location", required: false },
{ isDefault: true, required: true, slug: "title", type: "text" },
{ isDefault: true, required: false, slug: "notes", type: "textarea" },
{ isDefault: true, required: false, slug: "guests", type: "multiemail" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false },
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{
isDefault: true,
type: "radioInput",
slug: "location",
required: false,
disableOnPrefill: false,
hidden: false,
label: "",
placeholder: "",
},
{ isDefault: true, required: true, slug: "title", type: "text", disableOnPrefill: false, hidden: true },
{
isDefault: true,
required: false,
slug: "notes",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
required: false,
slug: "guests",
type: "multiemail",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
required: false,
slug: "rescheduleReason",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false, hidden: true },
];
beforeAll(async () => {
@@ -1382,6 +1435,8 @@ describe("Event types Endpoints", () => {
label: userDefinedBookingField.label,
required: userDefinedBookingField.required,
placeholder: userDefinedBookingField.placeholder,
disableOnPrefill: false,
hidden: false,
},
]);
});
@@ -1435,6 +1490,85 @@ describe("Event types Endpoints", () => {
});
});
it("should return event type with default bookingFields if they are not defined", async () => {
const eventTypeInput = {
title: "undefined booking fields",
description: "undefined booking fields",
length: 40,
hidden: false,
slug: "undefined-booking-fields",
locations: [],
schedulingType: SchedulingType.ROUND_ROBIN,
};
const eventType = await eventTypesRepositoryFixture.create(eventTypeInput, user.id);
return request(app.getHttpServer())
.get(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([
{
isDefault: true,
type: "name",
slug: "name",
required: true,
disableOnPrefill: false,
},
{
isDefault: true,
type: "email",
slug: "email",
required: true,
disableOnPrefill: false,
},
{
isDefault: true,
type: "radioInput",
slug: "location",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "text",
slug: "title",
required: true,
disableOnPrefill: false,
hidden: true,
},
{
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "multiemail",
slug: "guests",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "textarea",
slug: "rescheduleReason",
required: false,
disableOnPrefill: false,
hidden: false,
},
]);
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -34,6 +34,7 @@ import {
UpdateEventTypeInput_2024_06_14,
GetEventTypesQuery_2024_06_14,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
@Controller({
@@ -113,10 +114,26 @@ export class EventTypesController_2024_06_14 {
@Query() queryParams: GetEventTypesQuery_2024_06_14
): Promise<GetEventTypesOutput_2024_06_14> {
const eventTypes = await this.eventTypesService.getEventTypes(queryParams);
const eventTypesFormatted = this.eventTypeResponseTransformPipe.transform(eventTypes);
const eventTypesWithoutHiddenFields = eventTypesFormatted.map((eventType) => {
return {
...eventType,
bookingFields: Array.isArray(eventType?.bookingFields)
? eventType?.bookingFields
.map((field) => {
if ("hidden" in field) {
return field.hidden !== true ? field : null;
}
return field;
})
.filter((f) => f)
: [],
};
}) as EventTypeOutput_2024_06_14[];
return {
status: SUCCESS_STATUS,
data: this.eventTypeResponseTransformPipe.transform(eventTypes),
data: eventTypesWithoutHiddenFields,
};
}
@@ -1,4 +1,4 @@
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { ApiProperty as DocsProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsString, IsBoolean, IsOptional, IsEnum, Matches } from "class-validator";
@@ -38,34 +38,34 @@ export class CreatePhoneCallInput {
@IsOptional()
@IsString()
@DocsProperty({ description: "Scheduler name" })
@ApiPropertyOptional({ description: "Scheduler name" })
schedulerName?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => (value ? value : undefined))
@DocsProperty({ description: "Guest name" })
@ApiPropertyOptional({ description: "Guest name" })
guestName?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => (value ? value : undefined))
@DocsProperty({ description: "Guest email" })
@ApiPropertyOptional({ description: "Guest email" })
guestEmail?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => (value ? value : undefined))
@DocsProperty({ description: "Guest company" })
@ApiPropertyOptional({ description: "Guest company" })
guestCompany?: string;
@IsOptional()
@IsString()
@DocsProperty({ description: "Begin message" })
@ApiPropertyOptional({ description: "Begin message" })
beginMessage?: string;
@IsOptional()
@IsString()
@DocsProperty({ description: "General prompt" })
@ApiPropertyOptional({ description: "General prompt" })
generalPrompt?: string;
}
@@ -17,7 +17,7 @@ export class EventTypeResponseTransformPipe implements PipeTransform {
private transformEventType(eventType: EventTypeResponse): EventTypeOutput_2024_06_14 {
return plainToClass(
EventTypeOutput_2024_06_14,
this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType),
this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType, false),
{ strategy: "exposeAll" }
);
}
@@ -5,7 +5,7 @@ import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, BadRequestException } from "@nestjs/common";
import {
transformBookingFieldsApiToInternal,
transformBookingFieldsApiRequestToInternal,
transformLocationsApiToInternal,
transformIntervalLimitsApiToInternal,
transformFutureBookingLimitsApiToInternal,
@@ -218,7 +218,7 @@ export class InputEventTypesService_2024_06_14 {
hasMultipleLocations: boolean
) {
const customFields: (SystemField | CustomField)[] = inputBookingFields
? transformBookingFieldsApiToInternal(inputBookingFields)
? transformBookingFieldsApiRequestToInternal(inputBookingFields)
: [];
const customFieldsWithoutNameEmail = customFields.filter(
(field) => field.type !== "name" && field.type !== "email"
@@ -21,6 +21,7 @@ import {
transformSeatsInternalToApi,
InternalLocation,
BookingFieldSchema,
getBookingFieldsWithSystemFields,
} from "@calcom/platform-libraries";
import {
TransformFutureBookingsLimitSchema_2024_06_14,
@@ -88,7 +89,11 @@ type Input = Pick<
@Injectable()
export class OutputEventTypesService_2024_06_14 {
getResponseEventType(ownerId: number, databaseEventType: Input): EventTypeOutput_2024_06_14 {
getResponseEventType(
ownerId: number,
databaseEventType: Input,
isOrgTeamEvent: boolean
): EventTypeOutput_2024_06_14 {
const {
id,
length,
@@ -122,7 +127,7 @@ export class OutputEventTypesService_2024_06_14 {
const customName = databaseEventType?.eventName ?? undefined;
const bookingFields = databaseEventType.bookingFields
? this.transformBookingFields(databaseEventType.bookingFields)
: [];
: this.getDefaultBookingFields(isOrgTeamEvent);
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
const users = this.transformUsers(databaseEventType.users || []);
@@ -240,6 +245,18 @@ export class OutputEventTypesService_2024_06_14 {
return [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
}
getDefaultBookingFields(isOrgTeamEvent: boolean) {
const defaultBookingFields = getBookingFieldsWithSystemFields({
disableGuests: false,
bookingFields: null,
customInputs: [],
metadata: null,
workflows: [],
isOrgTeamEvent,
});
return this.transformBookingFields(defaultBookingFields);
}
transformRecurringEvent(recurringEvent: any) {
if (!recurringEvent) return null;
const recurringEventParsed = parseRecurringEvent(recurringEvent);
+14 -2
View File
@@ -1,39 +1,51 @@
import { ApiProperty as DocsProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsInt, IsEmail, IsOptional, IsString, ValidateNested } from "class-validator";
import { IsString, IsOptional, IsInt, IsEmail, ValidateNested } from "class-validator";
export class MeOrgOutput {
@DocsProperty()
isPlatform!: boolean;
@DocsProperty()
id!: number;
}
export class MeOutput {
@IsInt()
@DocsProperty()
id!: number;
@IsString()
@DocsProperty()
username!: string;
@IsEmail()
@DocsProperty()
email!: string;
@IsInt()
@DocsProperty()
timeFormat!: number;
@IsInt()
@IsOptional()
@DocsProperty({ type: Number, nullable: true })
defaultScheduleId!: number | null;
@IsString()
@DocsProperty()
weekStart!: string;
@IsString()
@DocsProperty()
timeZone!: string;
@IsInt()
@DocsProperty({ type: Number, nullable: true })
organizationId!: number | null;
@IsOptional()
@ValidateNested()
@Type(() => MeOrgOutput)
@ApiPropertyOptional({ type: MeOrgOutput })
organization?: MeOrgOutput;
}
@@ -9,6 +9,8 @@ import { ProviderModule } from "@/ee/provider/provider.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
import { SlotsModule } from "@/modules/slots/slots.module";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { TeamsModule } from "@/modules/teams/teams/teams.module";
import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
import { Module } from "@nestjs/common";
@@ -19,6 +21,7 @@ import { Module } from "@nestjs/common";
ProviderModule,
SchedulesModule_2024_04_15,
SchedulesModule_2024_06_11,
TeamsEventTypesModule,
MeModule,
EventTypesModule_2024_04_15,
EventTypesModule_2024_06_14,
@@ -27,6 +30,7 @@ import { Module } from "@nestjs/common";
BookingsModule_2024_08_13,
TeamsMembershipsModule,
SlotsModule,
TeamsModule,
],
})
export class PlatformEndpointsModule implements NestModule {
@@ -1,20 +1,25 @@
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsBoolean, IsTimeZone, IsOptional, IsString, ValidateNested } from "class-validator";
export class CreateScheduleInput_2024_04_15 {
@IsString()
@ApiProperty()
name!: string;
@IsTimeZone()
@ApiProperty()
timeZone!: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateAvailabilityInput_2024_04_15)
@IsOptional()
@ApiPropertyOptional({ type: [CreateAvailabilityInput_2024_04_15] })
availabilities?: CreateAvailabilityInput_2024_04_15[];
@IsBoolean()
@ApiProperty()
isDefault!: boolean;
}
@@ -1,95 +1,117 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsBoolean, IsInt, IsOptional, IsString, ValidateNested, IsArray } from "class-validator";
class EventTypeModel_2024_04_15 {
@IsInt()
@ApiProperty()
id!: number;
@IsOptional()
@IsString()
@ApiPropertyOptional({ type: String, nullable: true })
eventName?: string | null;
}
class AvailabilityModel_2024_04_15 {
@IsInt()
@ApiProperty()
id!: number;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
userId?: number | null;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
scheduleId?: number | null;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
eventTypeId?: number | null;
@IsArray()
@IsInt({ each: true })
@ApiProperty({ type: [Number] })
days!: number[];
@IsOptional()
@Type(() => Date)
@IsString() // Assuming date is serialized/deserialized appropriately
@IsString()
@ApiPropertyOptional()
startTime?: Date;
@IsOptional()
@Type(() => Date)
@IsString() // Assuming date is serialized/deserialized appropriately
@IsString()
@ApiPropertyOptional()
endTime?: Date;
@IsOptional()
@Type(() => Date)
@IsString() // Assuming date is serialized/deserialized appropriately
@IsString()
@ApiPropertyOptional({ type: String, nullable: true })
date?: Date | null;
}
class ScheduleModel_2024_04_15 {
@IsInt()
@ApiProperty()
id!: number;
@IsInt()
@ApiProperty()
userId!: number;
@IsString()
@ApiProperty()
name!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional({ type: String, nullable: true })
timeZone?: string | null;
@IsOptional()
@ValidateNested({ each: true })
@Type(() => EventTypeModel_2024_04_15)
@IsArray()
@ApiPropertyOptional({ type: [EventTypeModel_2024_04_15] })
eventType?: EventTypeModel_2024_04_15[];
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AvailabilityModel_2024_04_15)
@IsArray()
@ApiPropertyOptional({ type: [AvailabilityModel_2024_04_15] })
availability?: AvailabilityModel_2024_04_15[];
}
export class UpdatedScheduleOutput_2024_04_15 {
@ValidateNested()
@Type(() => ScheduleModel_2024_04_15)
@ApiProperty({ type: ScheduleModel_2024_04_15 })
schedule!: ScheduleModel_2024_04_15;
@IsBoolean()
@ApiProperty()
isDefault!: boolean;
@IsOptional()
@IsString()
@ApiPropertyOptional()
timeZone?: string;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
prevDefaultId?: number | null;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
currentDefaultId?: number | null;
}
@@ -1,105 +1,132 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsDate, IsOptional, IsArray, IsBoolean, IsInt, IsString, ValidateNested } from "class-validator";
class AvailabilityModel {
@IsInt()
@ApiProperty()
id!: number;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
userId?: number | null;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
eventTypeId?: number | null;
@IsArray()
@IsInt({ each: true })
@ApiProperty({ type: [Number] })
days!: number[];
@IsDate()
@Type(() => Date)
@ApiProperty({ type: Date })
startTime!: Date;
@IsDate()
@Type(() => Date)
@ApiProperty({ type: Date })
endTime!: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
@ApiPropertyOptional({ type: Date, nullable: true })
date?: Date | null;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
scheduleId?: number | null;
}
class WorkingHours {
@IsArray()
@IsInt({ each: true })
@ApiProperty({ type: [Number] })
days!: number[];
@IsInt()
@ApiProperty()
startTime!: number;
@IsInt()
@ApiProperty()
endTime!: number;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
userId?: number | null;
}
class TimeRange {
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
userId?: number | null;
@IsDate()
@ApiProperty({ type: Date })
start!: Date;
@IsDate()
@ApiProperty({ type: Date })
end!: Date;
}
export class ScheduleOutput {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty()
name!: string;
@IsBoolean()
@ApiProperty()
isManaged!: boolean;
@ValidateNested({ each: true })
@Type(() => WorkingHours)
@ApiProperty({ type: [WorkingHours] })
workingHours!: WorkingHours[];
@ValidateNested({ each: true })
@Type(() => AvailabilityModel)
@IsArray()
@ApiProperty({ type: [AvailabilityModel] })
schedule!: AvailabilityModel[];
@ApiProperty({ type: [[TimeRange]] })
availability!: TimeRange[][];
@IsString()
@ApiProperty()
timeZone!: string;
@ValidateNested({ each: true })
@IsArray()
@ApiPropertyOptional({ type: [Object] })
// note(Lauris) it should be
// dateOverrides!: { ranges: TimeRange[] }[];
// but docs aren't generating correctly it results in array of strings
dateOverrides!: unknown[];
@IsBoolean()
@ApiProperty()
isDefault!: boolean;
@IsBoolean()
@ApiProperty()
isLastSchedule!: boolean;
@IsBoolean()
@ApiProperty()
readOnly!: boolean;
}
@@ -10,7 +10,7 @@ export class OutputSchedulesService_2024_06_11 {
async getResponseSchedule(databaseSchedule: Schedule & { availability: Availability[] }) {
if (!databaseSchedule.timeZone) {
throw new Error("Failed to create schedule because its timezone is not set.");
databaseSchedule.timeZone = "Europe/London";
}
const ownerDefaultScheduleId = await this.usersRepository.getUserScheduleDefaultId(
+3 -1
View File
@@ -30,12 +30,14 @@ export type Environment = {
RATE_LIMIT_DEFAULT_BLOCK_DURATION_MS: number;
AXIOM_DATASET: string;
AXIOM_TOKEN: string;
STRIPE_TEAM_MONTHLY_PRICE_ID: string;
IS_TEAM_BILLING_ENABLED: boolean;
};
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
const value = process.env[key] as Environment[K] | undefined;
if (!value) {
if (value === undefined) {
if (fallback) {
return fallback;
}
@@ -9,7 +9,13 @@ import {
} from "@prisma/client/runtime/library";
import { Request } from "express";
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
import {
BAD_REQUEST,
CONFLICT,
ERROR_STATUS,
INTERNAL_SERVER_ERROR,
NOT_FOUND,
} from "@calcom/platform-constants";
import { Response } from "@calcom/platform-types";
type PrismaError =
@@ -43,11 +49,37 @@ export class PrismaExceptionFilter implements ExceptionFilter {
method: request.method,
requestId,
});
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
let message = "There was an error, please try again later.";
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
let errorCode = INTERNAL_SERVER_ERROR;
if (error instanceof PrismaClientKnownRequestError) {
switch (error.code) {
case "P2002": // Unique constraint failed
errorCode = CONFLICT;
statusCode = HttpStatus.CONFLICT;
message = "Invalid Input: Trying to create a record that already exists.";
break;
case "P2025": // Record not found
errorCode = NOT_FOUND;
statusCode = HttpStatus.NOT_FOUND;
message = "Invalid Query: The requested record was not found.";
break;
case "P2003": // Foreign key constraint failed
errorCode = BAD_REQUEST;
statusCode = HttpStatus.BAD_REQUEST;
message = "Invalid input: The referenced data does not exist.";
break;
default:
break;
}
}
response.status(statusCode).json({
status: ERROR_STATUS,
timestamp: new Date().toISOString(),
path: request.url,
error: { code: INTERNAL_SERVER_ERROR, message: "There was an error, please try again later." },
error: { code: errorCode, message: message },
});
}
}
@@ -0,0 +1,65 @@
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
describe("isOriginAllowed", () => {
describe("is allowed", () => {
it("should return true for exact match without wildcard", () => {
const allowedOrigins = ["https://app.cal.com/callback"];
const origin = "https://app.cal.com/callback";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should return true for wildcard domain match", () => {
const allowedOrigins = ["*.multiscreen.d1test.biz"];
const origin = "https://sub.multiscreen.d1test.biz";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should return true for wildcard pattern matching any domain", () => {
const allowedOrigins = ["*/callback"];
const origin = "https://another.com/callback";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should handle a wildcard only pattern", () => {
const allowedOrigins = ["*"];
const origin = "https://any.domain.com/anypath";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should handle multiple allowed URIs where only one matches", () => {
const allowedOrigins = [
"https://app.cal.com/other",
"*.notthisone.com",
"https://app.cal.com/callback",
];
const origin = "https://app.cal.com/callback";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should handle patterns with multiple wildcards correctly", () => {
const allowedOrigins = ["https://*.mydomain.com/*"];
const origin = "https://sub.mydomain.com/path/to/resource";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
});
describe("is not allowed", () => {
it("should return false if no allowed patterns match", () => {
const allowedOrigins = ["https://app.cal.com/callback", "*.multiscreen.d1test.biz", "*/callback"];
const origin = "https://unknown.com";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(false);
});
it("should return false if wildcard pattern doesn't match the given origin", () => {
const allowedOrigins = ["*.example.com"];
const origin = "https://notexample.org";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(false);
});
it("should handle empty allowedUris array correctly", () => {
const allowedOrigins: string[] = [];
const origin = "https://app.cal.com/callback";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(false);
});
});
});
@@ -0,0 +1,18 @@
export function isOriginAllowed(origin: string, allowedOrigins: string[]): boolean {
return allowedOrigins.some((allowedOrigin) => {
if (allowedOrigin.includes("*")) {
return wildcardToRegex(allowedOrigin).test(origin);
}
return origin === allowedOrigin;
});
}
function wildcardToRegex(pattern: string): RegExp {
const escaped = escapeRegex(pattern);
const regexPattern = "^" + escaped.replace(/\\\*/g, ".*") + "$";
return new RegExp(regexPattern);
}
function escapeRegex(str: string): string {
return str.replace(/[.+*?^${}()|[\]\\]/g, "\\$&");
}
@@ -0,0 +1,12 @@
export const defaultBookingResponses = {
name: "unknown",
email: "unknown",
guests: [],
rescheduleReason: "unknown",
};
export const defaultBookingMetadata = { videoCallUrl: "unknown" };
export const defaultSeatedBookingData = { responses: { name: "unknown", email: "unknown" } };
export const defaultSeatedBookingMetadata = {};
@@ -0,0 +1,23 @@
import { Logger } from "@nestjs/common";
import { ZodSchema } from "zod";
const logger = new Logger("safeParse");
export function safeParse<T>(schema: ZodSchema<T>, value: unknown, defaultValue: T): T {
const result = schema.safeParse(value);
if (result.success) {
return result.data;
} else {
const errorStack = new Error().stack;
logger.error(
`Zod parsing failed.\n` +
`1. Schema: ${schema.description || "UnnamedSchema"}\n` +
`2. Input: ${JSON.stringify(value, null, 2)}\n` +
`3. Zod Error: ${result.error}\n` +
`4. Call Stack: ${errorStack}`
);
return defaultValue;
}
}
@@ -8,12 +8,13 @@ import { CredentialsRepository } from "@/modules/credentials/credentials.reposit
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule],
imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule, TeamsEventTypesModule],
providers: [
EventTypesAtomService,
ConferencingAtomsService,
@@ -2,6 +2,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { credentialForCalendarServiceSelect } from "@calcom/platform-libraries";
import { paymentDataSelect } from "@calcom/platform-libraries";
@Injectable()
@@ -14,4 +15,53 @@ export class AtomsRepository {
select: paymentDataSelect,
});
}
async getUserTeams(userId: number) {
const userTeams = await this.dbRead.prisma.team.findMany({
where: {
members: {
some: {
userId,
accepted: true,
},
},
},
select: {
id: true,
credentials: {
select: credentialForCalendarServiceSelect,
},
name: true,
logoUrl: true,
members: {
where: {
userId,
},
select: {
role: true,
},
},
parent: {
select: {
id: true,
credentials: {
select: credentialForCalendarServiceSelect,
},
name: true,
logoUrl: true,
members: {
where: {
userId,
},
select: {
role: true,
},
},
},
},
},
});
return userTeams;
}
}
@@ -1,10 +1,12 @@
import { Transform, Type } from "class-transformer";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { ArrayNotEmpty, IsArray, IsInt, IsNumber, IsOptional } from "class-validator";
export class EventTypesAppInput {
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@IsOptional()
@ApiPropertyOptional()
teamId?: number;
}
@@ -12,5 +14,6 @@ export class BulkUpdateEventTypeToDefaultLocationDto {
@IsArray()
@ArrayNotEmpty()
@IsInt({ each: true })
@ApiProperty({ type: [Number] })
eventTypeIds!: number[];
}
@@ -2,8 +2,7 @@ import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UsersService } from "@/modules/users/services/users.service";
@@ -46,12 +45,11 @@ export class EventTypesAtomService {
private readonly membershipsRepository: MembershipsRepository,
private readonly credentialsRepository: CredentialsRepository,
private readonly atomsRepository: AtomsRepository,
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository,
private readonly usersService: UsersService,
private readonly dbWrite: PrismaWriteService,
private readonly dbRead: PrismaReadService,
private readonly eventTypeService: EventTypesService_2024_06_14,
private readonly teamEventTypeService: OrganizationsEventTypesService
private readonly teamEventTypeService: TeamsEventTypesService
) {}
async getUserEventType(user: UserWithProfile, eventTypeId: number) {
@@ -183,7 +181,7 @@ export class EventTypesAtomService {
let credentials = await this.credentialsRepository.getAllUserCredentialsById(userId);
let userTeams: TeamQuery[] = [];
if (teamId) {
const teamsQuery = await this.organizationsTeamsRepository.getUserTeamsById(userId);
const teamsQuery = await this.atomsRepository.getUserTeams(userId);
// If a team is a part of an org then include those apps
// Don't want to iterate over these parent teams
const filteredTeams: TeamQuery[] = [];
@@ -1,22 +1,18 @@
import { UserWithProfile } from "@/modules/users/users.repository";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { ExecutionContext } from "@nestjs/common";
import { createParamDecorator } from "@nestjs/common";
export type GetUserReturnType = UserWithProfile & { isSystemAdmin: boolean };
export const GetUser = createParamDecorator<
keyof GetUserReturnType | (keyof GetUserReturnType)[],
keyof ApiAuthGuardUser | (keyof ApiAuthGuardUser)[],
ExecutionContext
>((data, ctx) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user as GetUserReturnType;
const user = request.user as ApiAuthGuardUser;
if (!user) {
throw new Error("GetUser decorator : User not found");
}
user.isSystemAdmin = user.role === "ADMIN";
if (Array.isArray(data)) {
return data.reduce((prev, curr) => {
return {
@@ -1,5 +1,5 @@
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { PlatformPlanType } from "@/modules/billing/types";
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { RedisService } from "@/modules/redis/redis.service";
@@ -19,7 +19,7 @@ export class PlatformPlanGuard implements CanActivate {
const request = context.switchToHttp().getRequest<Request>();
const teamId = request.params.teamId as string;
const orgId = request.params.orgId as string;
const user = request.user as GetUserReturnType;
const user = request.user as ApiAuthGuardUser;
const minimumPlan = this.reflector.get(PlatformPlan, context.getHandler()) as PlatformPlanType;
const REDIS_CACHE_KEY = `apiv2:user:${user?.id ?? "none"}:org:${orgId ?? "none"}:team:${
@@ -1,6 +1,6 @@
import { ORG_ROLES, TEAM_ROLES, SYSTEM_ADMIN_ROLE } from "@/lib/roles/constants";
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException, Logger } from "@nestjs/common";
@@ -22,7 +22,7 @@ export class RolesGuard implements CanActivate {
const request = context.switchToHttp().getRequest<Request & { team: Team }>();
const teamId = request.params.teamId as string;
const orgId = request.params.orgId as string;
const user = request.user as GetUserReturnType;
const user = request.user as ApiAuthGuardUser;
const allowedRole = this.reflector.get(Roles, context.getHandler());
const REDIS_CACHE_KEY = `apiv2:user:${user.id ?? "none"}:org:${orgId ?? "none"}:team:${
teamId ?? "none"
@@ -1,5 +1,6 @@
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { AuthMethods } from "@/lib/enums/auth-methods";
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
import { BaseStrategy } from "@/lib/passport/strategies/types";
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
import { DeploymentsService } from "@/modules/deployments/deployments.service";
@@ -16,6 +17,8 @@ import { getToken } from "next-auth/jwt";
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
@Injectable()
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
constructor(
@@ -74,12 +77,19 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
async authenticateNextAuth(token: { email?: string | null }) {
const user = await this.nextAuthStrategy(token);
return this.success(user);
return this.success(this.getSuccessUser(user));
}
getSuccessUser(user: UserWithProfile): ApiAuthGuardUser {
return {
...user,
isSystemAdmin: user.role === "ADMIN",
};
}
async authenticateOAuthClient(oAuthClientId: string, oAuthClientSecret: string) {
const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret);
return this.success(user);
return this.success(this.getSuccessUser(user));
}
async oAuthClientStrategy(oAuthClientId: string, oAuthClientSecret: string) {
@@ -118,7 +128,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
return this.error(new UnauthorizedException("No user associated with the provided token"));
}
return this.success(user);
return this.success(this.getSuccessUser(user));
} catch (err) {
if (err instanceof Error) {
return this.error(err);
@@ -167,7 +177,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
throw new UnauthorizedException("OAuth client not found given the access token");
}
if (origin && !client.redirectUris.some((uri) => uri.startsWith(origin))) {
if (origin && !isOriginAllowed(origin, client.redirectUris)) {
throw new UnauthorizedException(
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${origin}' to the 'Redirect uris' of your OAuth client.`
);
@@ -3,6 +3,7 @@ import { ConferencingController } from "@/modules/conferencing/controllers/confe
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service";
import { Office365VideoService } from "@/modules/conferencing/services/office365-video.service";
import { ZoomVideoService } from "@/modules/conferencing/services/zoom-video.service";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
@@ -21,6 +22,7 @@ import { ConfigModule } from "@nestjs/config";
UsersRepository,
TokensRepository,
ZoomVideoService,
Office365VideoService,
AppsRepository,
],
exports: [],
@@ -15,6 +15,7 @@ import { GetDefaultConferencingAppOutputResponseDto } from "@/modules/conferenci
import { SetDefaultConferencingAppOutputResponseDto } from "@/modules/conferencing/outputs/set-default-conferencing-app.output";
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service";
import { Office365VideoService } from "@/modules/conferencing/services/office365-video.service";
import { ZoomVideoService } from "@/modules/conferencing/services/zoom-video.service";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UserWithProfile } from "@/modules/users/users.repository";
@@ -39,7 +40,7 @@ import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToInstance } from "class-transformer";
import { Request } from "express";
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS } from "@calcom/platform-constants";
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS, OFFICE_365_VIDEO } from "@calcom/platform-constants";
export type OAuthCallbackState = {
accessToken: string;
@@ -61,7 +62,8 @@ export class ConferencingController {
private readonly tokensRepository: TokensRepository,
private readonly conferencingService: ConferencingService,
private readonly googleMeetService: GoogleMeetService,
private readonly zoomVideoService: ZoomVideoService
private readonly zoomVideoService: ZoomVideoService,
private readonly office365VideoService: Office365VideoService
) {}
@Post("/:app/connect")
@@ -116,8 +118,18 @@ export class ConferencingController {
data: plainToInstance(ConferencingAppsOauthUrlOutputDto, credential),
};
case OFFICE_365_VIDEO:
credential = await this.office365VideoService.generateOffice365AuthUrl(JSON.stringify(state));
return {
status: SUCCESS_STATUS,
data: plainToInstance(ConferencingAppsOauthUrlOutputDto, credential),
};
default:
throw new BadRequestException("Invalid conferencing app, available apps are: ", [ZOOM].join(", "));
throw new BadRequestException(
"Invalid conferencing app, available apps are: ",
[ZOOM, OFFICE_365_VIDEO].join(", ")
);
}
}
@@ -147,8 +159,14 @@ export class ConferencingController {
case ZOOM:
return await this.zoomVideoService.connectZoomApp(decodedCallbackState, code, userId);
case OFFICE_365_VIDEO:
return await this.office365VideoService.connectOffice365App(decodedCallbackState, code, userId);
default:
throw new BadRequestException("Invalid conferencing app, available apps are: ", [ZOOM].join(", "));
throw new BadRequestException(
"Invalid conferencing app, available apps are: ",
[ZOOM, OFFICE_365_VIDEO].join(", ")
);
}
} catch (error) {
return {
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsString, ValidateNested, IsEnum, IsNumber, IsOptional, IsBoolean } from "class-validator";
@@ -20,7 +20,11 @@ export class ConferencingAppsOutputDto {
@IsNumber()
userId!: number;
@ApiProperty({ example: true, description: "Whether if the connection is working or not." })
@ApiPropertyOptional({
example: true,
description: "Whether if the connection is working or not.",
nullable: true,
})
@Expose()
@IsBoolean()
@IsOptional()
@@ -28,24 +32,26 @@ export class ConferencingAppsOutputDto {
}
export class ConferencingAppsOutputResponseDto {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@ApiProperty({ type: String, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@Expose()
@ValidateNested()
@ValidateNested({ each: true })
@Type(() => ConferencingAppsOutputDto)
@ApiProperty({ type: [ConferencingAppsOutputDto] })
data!: ConferencingAppsOutputDto[];
}
export class ConferencingAppOutputResponseDto {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@ApiProperty({ type: String, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@Expose()
@ValidateNested()
@Type(() => ConferencingAppsOutputDto)
@ApiProperty({ type: ConferencingAppsOutputDto })
data!: ConferencingAppsOutputDto;
}
@@ -0,0 +1,121 @@
import { AppsRepository } from "@/modules/apps/apps.repository";
import { OAuthCallbackState } from "@/modules/conferencing/controllers/conferencing.controller";
import { BadRequestException, Logger, NotFoundException } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import type { Prisma } from "@prisma/client";
import { z } from "zod";
import { OFFICE_365_VIDEO, OFFICE_365_VIDEO_TYPE } from "@calcom/platform-constants";
import stringify = require("qs-stringify");
const zoomAppKeysSchema = z.object({
client_id: z.string(),
client_secret: z.string(),
});
@Injectable()
export class Office365VideoService {
private logger = new Logger("Office365VideoService");
private redirectUri = `${this.config.get("api.url")}/conferencing/${OFFICE_365_VIDEO}/oauth/callback`;
private scopes = ["OnlineMeetings.ReadWrite", "offline_access"];
constructor(private readonly config: ConfigService, private readonly appsRepository: AppsRepository) {}
async getOffice365AppKeys() {
const app = await this.appsRepository.getAppBySlug(OFFICE_365_VIDEO);
const { client_id, client_secret } = zoomAppKeysSchema.parse(app?.keys);
if (!client_id) {
throw new NotFoundException("Office365 app not found");
}
if (!client_secret) {
throw new NotFoundException("Office365 app not found");
}
return { client_id, client_secret };
}
async generateOffice365AuthUrl(state: string) {
const { client_id } = await this.getOffice365AppKeys();
const params = {
response_type: "code",
client_id,
scope: this.scopes.join(" "),
redirect_uri: this.redirectUri,
state: state,
};
const query = stringify(params);
const url = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${query}`;
return { url };
}
async connectOffice365App(state: OAuthCallbackState, code: string, userId: number) {
const { client_id, client_secret } = await this.getOffice365AppKeys();
const toUrlEncoded = (payload: Record<string, string>) =>
Object.keys(payload)
.map((key) => `${key}=${encodeURIComponent(payload[key])}`)
.join("&");
const body = toUrlEncoded({
client_id,
grant_type: "authorization_code",
code,
scope: this.scopes.join(" "),
redirect_uri: this.redirectUri,
client_secret,
});
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
body,
});
const responseBody = await response.json();
if (!response.ok) {
throw new BadRequestException(responseBody.error);
}
const whoami = await fetch("https://graph.microsoft.com/v1.0/me", {
headers: { Authorization: `Bearer ${responseBody.access_token}` },
});
const graphUser = await whoami.json();
// In some cases, graphUser.mail is null. Then graphUser.userPrincipalName most likely contains the email address.
responseBody.email = graphUser.mail ?? graphUser.userPrincipalName;
responseBody.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in); // set expiry date in seconds
delete responseBody.expires_in;
const existingCredentialOffice365Video = await this.appsRepository.findAppCredential({
type: OFFICE_365_VIDEO_TYPE,
userId,
appId: OFFICE_365_VIDEO,
});
const credentialIdsToDelete = existingCredentialOffice365Video.map((item) => item.id);
if (credentialIdsToDelete.length > 0) {
await this.appsRepository.deleteAppCredentials(credentialIdsToDelete, userId);
}
await this.appsRepository.createAppCredential(
OFFICE_365_VIDEO_TYPE,
responseBody as unknown as Prisma.InputJsonObject,
userId,
OFFICE_365_VIDEO
);
return { url: state.returnTo ?? "" };
}
}
@@ -18,7 +18,7 @@ const zoomAppKeysSchema = z.object({
@Injectable()
export class ZoomVideoService {
private logger = new Logger("ZoomVideoService");
private redirectUri = `${this.config.get("api.url")}/conferencing/zoom/oauth/callback`;
private redirectUri = `${this.config.get("api.url")}/conferencing/${ZOOM}/oauth/callback`;
constructor(private readonly config: ConfigService, private readonly appsRepository: AppsRepository) {}
@@ -33,6 +33,16 @@ export class MembershipsRepository {
return membership;
}
async findUserMemberships(userId: number) {
const memberships = await this.dbRead.prisma.membership.findMany({
where: {
userId,
},
});
return memberships;
}
async findMembershipByOrgId(orgId: number, userId: number) {
return this.findMembershipByTeamId(orgId, userId);
}
@@ -10,10 +10,10 @@ export class ManagedUserOutput {
@ApiProperty({ example: "alice+cluo37fwd0001khkzqqynkpj3@example.com" })
email!: string;
@ApiProperty({ example: "alice" })
@ApiProperty({ example: "alice", nullable: true })
username!: string | null;
@ApiProperty({ example: "alice" })
@ApiProperty({ example: "alice", nullable: true })
name!: string | null;
@ApiProperty({ example: "America/New_York" })
@@ -22,19 +22,19 @@ export class ManagedUserOutput {
@ApiProperty({ example: "Sunday" })
weekStart!: string;
@ApiProperty({ example: "2024-04-01T00:00:00.000Z", type: "string" })
@ApiProperty({ type: String, example: "2024-04-01T00:00:00.000Z" })
@Transform(({ value }) => value.toISOString())
createdDate!: Date;
@ApiProperty({ example: 12, nullable: true })
@ApiProperty({ type: Number, example: 12, nullable: true })
timeFormat!: number | null;
@ApiProperty({ example: null })
@ApiProperty({ type: Number, example: null, nullable: true })
defaultScheduleId!: number | null;
@IsEnum(Locales)
@IsOptional()
@ApiProperty({ example: Locales.EN, enum: Locales })
@ApiPropertyOptional({ type: String, example: Locales.EN, enum: Locales })
locale?: Locales;
@IsUrl()
@@ -43,7 +43,7 @@ export class ManagedUserOutput {
type: String,
example: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
description: `URL of the user's avatar image`,
required: false,
nullable: true,
})
avatarUrl?: string | null;
}
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsArray,
@@ -30,7 +30,7 @@ export class PlatformOAuthClientDto {
@IsNumber()
permissions!: number;
@ApiProperty({ example: "https://example.com/logo.png", required: false })
@ApiPropertyOptional({ example: "https://example.com/logo.png" })
@IsOptional()
@IsString()
logo!: string | null;
@@ -54,9 +54,7 @@ export class GetOAuthClientResponseDto {
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: PlatformOAuthClientDto,
})
@ApiProperty({ type: PlatformOAuthClientDto })
@IsNotEmptyObject()
@ValidateNested()
@Type(() => PlatformOAuthClientDto)
@@ -1,5 +1,6 @@
import { getEnv } from "@/env";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
@@ -62,7 +63,7 @@ export class OAuthFlowController {
throw new BadRequestException(`OAuth client with ID '${clientId}' not found`);
}
if (!oauthClient?.redirectUris.includes(body.redirectUri)) {
if (!isOriginAllowed(body.redirectUri, oauthClient.redirectUris)) {
throw new BadRequestException("Invalid 'redirect_uri' value.");
}
@@ -1,4 +1,4 @@
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { UsersService } from "@/modules/users/services/users.service";
import {
@@ -15,8 +15,8 @@ export class OAuthClientGuard implements CanActivate {
constructor(private oAuthClientRepository: OAuthClientRepository, private usersService: UsersService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request & { user: GetUserReturnType }>();
const user: GetUserReturnType = request.user;
const request = context.switchToHttp().getRequest<Request & { user: ApiAuthGuardUser }>();
const user: ApiAuthGuardUser = request.user;
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
const oAuthClientId = request.params.clientId;
@@ -1,32 +1,40 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsArray, IsBoolean, IsOptional, IsString } from "class-validator";
export class UpdateOAuthClientInput {
@IsOptional()
@IsString()
@ApiPropertyOptional()
logo?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
name?: string;
@IsArray()
@IsOptional()
@IsString({ each: true })
redirectUris?: string[] = [];
@ApiPropertyOptional({ type: [String] })
redirectUris?: string[];
@IsOptional()
@IsString()
@ApiPropertyOptional()
bookingRedirectUri?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
bookingCancelRedirectUri?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
bookingRescheduleRedirectUri?: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
areEmailsEnabled?: boolean;
}
@@ -1,6 +1,5 @@
import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input";
import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
@@ -11,15 +10,15 @@ import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-a
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
import { CreateTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/create-team-event-type.output";
import { DeleteTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/delete-team-event-type.output";
import { GetTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-type.output";
import { GetTeamEventTypesOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-types.output";
import { UpdateTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/update-team-event-type.output";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer";
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
import { CreateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/create-team-event-type.output";
import { DeleteTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/delete-team-event-type.output";
import { GetTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/get-team-event-type.output";
import { GetTeamEventTypesOutput } from "@/modules/teams/event-types/outputs/get-team-event-types.output";
import { UpdateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/update-team-event-type.output";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
@@ -62,7 +61,6 @@ export class OrganizationsEventTypesController {
constructor(
private readonly organizationsEventTypesService: OrganizationsEventTypesService,
private readonly inputService: InputOrganizationsEventTypesService,
private readonly inputUserEventTypesService: InputEventTypesService_2024_06_14,
private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe
) {}
@@ -180,7 +178,11 @@ export class OrganizationsEventTypesController {
@Query() queryParams: SkipTakePagination
): Promise<GetTeamEventTypesOutput> {
const { skip, take } = queryParams;
const eventTypes = await this.organizationsEventTypesService.getTeamsEventTypes(orgId, skip, take);
const eventTypes = await this.organizationsEventTypesService.getOrganizationsTeamsEventTypes(
orgId,
skip,
take
);
return {
status: SUCCESS_STATUS,

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