Compare commits

...
102 Commits
Author SHA1 Message Date
Omar López 60ae817f30 chore: v4.0.3 (#14725)
release.yml / release (push) Canceled after 0s
Bump version
2024-04-23 22:53:24 +00:00
Udit Takkar 74166fc2cb fix: settings layout for org admin (#14723) 2024-04-23 21:53:17 +00:00
Patrick Göler von Ravensburg fb997045f2 feat: Add app store entry for "Autocheckin" (Challenge/Bounty) (#14683) 2024-04-23 21:52:03 +00:00
Keith Williams 73a79cf29b fix: Improve French translations (#14718) 2024-04-23 17:35:56 -04:00
Omar López 54a1663a8b v4.0.2 (#14724) 2024-04-23 21:16:14 +00:00
883d745784 feat: SMS/Whatsapp to attendee for teams (#14648)
* remove event type requires confirmation

* allow sms sending also for team (frontend)

* allow text to attendee for team (backend)

* add rate limiting for SMS (WIP)

* add functionality to lock and unlock SMS sending (+admin UI view)

* rename to smsLockState

* add ability to lock users and teams

* don't send sms if locked

* add missing imports

* fix rate limit namespace

* add translation

* fix type error

* remove prop from UsersTable

* add smsLockState to buildUser

* code clean up

* create seperate sms rate limit function

* fix type error

* add missing userId and teamId to sendSMS

* code improvements

* add User Team type

* fix marking as reviewed

* check monthly sms limit only for user

* don't lock orgs

* fix type error

* fix avatars

* fix type error

* fix type error

* fix type error

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-23 19:48:44 +00:00
571e93a19a chore: EventManager add cancelEvent method (#14261)
* fix timezone display on booking page to reflect event availability timezone

* migrate fetching event owner's schedule to server side

* migrate fetching event owner's schedule to server side

* fix e2e test errors

* Add WEBAPP_URL_FOR_OAUTH to salesforce auth

* In event manager constructor include "_crm" credentials as calendar creds

* Change crm apps to type to end with `_crm`

* Move sendgrid out of CRM

* Add zoho bigin to CRM apps

* When getting apps, use slug

* Add `crm` variants

* Hubspot Oauth use `WEBAPP_URL_FOR_OAUTH`

* Refactor creating credentials

* Fix empty CRM page

* Use credentials with `_crm`

* Abstract getAppCategoryTitle

* Add integration.handler changes

* When searching for credential, look for current credentials in class

* On cancel, delete 3rd party events

* Fix tests

* Type fix

* Type fixes

* Remove apiDeletes

* Type fixes

* Specific typing

---------

Co-authored-by: Shaik-Sirajuddin <sirajuddinshaik30gmail.com>
Co-authored-by: Shaik-Sirajuddin <sirajudddinshaik30@gmail.com>
Co-authored-by: Shaik-Sirajuddin <89742297+Shaik-Sirajuddin@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
2024-04-23 17:54:45 +00:00
Peer RichelsenandUdit Takkar d16428da6c changed translation (#14710)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-23 16:14:12 +00:00
sean-brydon 8c5438663c feat: improve error handling for signup events (#14668)
* feat: improve error handling for signup events

* chore: fix typo
2024-04-23 16:13:10 +00:00
Hariom Balhara ed76d163cf fix: Add navigatedToBooker event and fix infinite loop of events (#14694)
## What does this PR do?

1. cal.com/sales page is a special case where embed-iframe exists in top as well as in iframe. This causes a looping of events somehow due to embed-iframe firing events and those being received by the top embed-iframe.
2. Added new `navigatedToBooker` event to know when the user lands on booker page.


Corresponding PR in website that uses `navigatedToBooker` event  and has a demo as well https://github.com/calcom/website/pull/741

Earlier added `routed` event seems to happen too early

## Type of change
- Bug fix (non-breaking change which fixes an issue)

## How should this be tested?
- Simply navigate to booker and wait for the event.

## Mandatory Tasks
- [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.

## Tests
- Didn't add any tests for this as the changed seems straightforward

## Checklist
- I haven't added tests that prove my fix is effective or that my feature works
2024-04-23 10:36:12 +01:00
9215725dad fix: browser back button not working (#13345)
* fixed the browser back button not woking

* fix back button

* added shouldReplace param to query-params helper.

* revert yarn.lock

* fix: test

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-04-22 18:42:54 -04:00
Alex van Andel 00131668aa fix: Avatar API isn't ran at signup handler (#14675)
release.yml / release (push) Canceled after 0s
2024-04-22 12:13:15 -03:00
Alex van Andel 0306ad737f fix: invalid Cancelled / Rescheduled downstream calendar block (#14700)
* fix: invalid Cancelled / Rescheduled downstream calendar block

* Adjust test so no calendar is connected

* Remove the googleCalendar from multiple tests
2024-04-22 16:10:39 +01:00
20dfc34e70 fix: Managed event isn’t showing in the upcoming bookings when filtered (#14552)
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-22 10:55:26 -04:00
Somay Chauhan b7e8b252d2 fix: send completed_onboarding to intercom (#14703) 2024-04-22 14:12:02 +00:00
74f09c332b feat: updated the useage of listCurrent in the app and use org role from session (#14653)
* feat: include org role in org session data

* update contents

* fix: type err

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-04-22 14:07:43 +00:00
sean-brydonandHariom 3ce6c23603 fix: Update visibility of team members based on user role (#14629)
* fix: team memebrs being hidden for admin when org is private and they are not org admin

* chore: remove console.loog

* chore: make permissionCheck more readable

* test: push e2e test for normal privacy banner in orgs

* test: add a few more edge cases

* chore: negate names

* feat: update func to make more sense

* Fix

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-04-22 13:45:28 +00:00
Soumik PatraandUdit Takkar ad3ee14501 fix: Date Override Modal Trims Date Picker (#14680)
* [CAL-3500] Date Override Modal Trims Date Picker #14658

* file

* type fix

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-22 18:53:16 +05:30
Somay ChauhanandUdit Takkar 1931758fc7 fix: Private settings of the event disabled after making changes to your event (#14628)
* fix: Private settings of the event disabled after making changes to your event

* add test

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-22 11:53:50 +00:00
Kiss Benedek MátéandUdit Takkar fe551af145 Add missing dollar sign (#14681)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-22 11:33:07 +00:00
9fdaaed100 fix: show toast in center of screen (#14690)
* fix: show toast in center of screen

* fix: show toast in center of screen

---------

Co-authored-by: Nur Ovezova <nur.ovezova@techguilds.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-22 10:34:29 +00:00
Udit Takkar c0f8a355e3 fix: managed type url in orgs (#14698) 2024-04-22 09:38:58 +01:00
Peer Richelsen df95ec4ce8 fixed credit card icon (#14678) 2024-04-20 14:46:23 +01:00
Alex van Andel a15cad4d03 chore: Removing away (DB related, not slot related) from code (#14673)
* chore: Removing away (DB related, not slot related) from code

* Remove more isAway/away, eg. references
2024-04-20 02:23:34 +01:00
Udit Takkar 8904f20eb8 fix: get llm response schema (#14670) 2024-04-19 18:09:16 +00:00
Carina WollendorferandCarinaWolli 0ee81d0739 fix: remove away view (#14672)
* remove away view

* remove yellow away dot

* remove Away from Booker

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-04-19 17:40:35 +00:00
Peer Richelsen c0bc6f8ccb Update common.json (#14647) 2024-04-19 16:50:38 +00:00
Hariom Balhara 1f1cd1e236 Add self serve variable in tests (#14665) 2024-04-19 12:32:22 +01:00
sean-brydon dbbd78fb26 feat: include org role in org session data (#14652)
* feat: include org role in org session data

* update contents

* remove: files
2024-04-19 12:32:04 +01:00
Somay Chauhan f6e45842fd fix: only boot intercom if .env variable is set (#14656) 2024-04-19 09:04:02 +01:00
Alex van Andel d25c043046 chore: Remove all avatar/logo references (#14532)
* chore: Remove all avatar/logo references

* Updated user profile to only use avatarUrl

* fix: minor style issue

* chore: Remove redundant includeTeamLogo

* fix: Use right avatar default in event type list

* fix: placeholder avatar team profile, target

* chore: Add logoUrl/avatarUrl to JWT

* fix: Bunch of org avatar issues, fix members list

* Fix logoUrl on org pages

* More type fixes

* Hopefully final type fixes

* Another round of type fixes

* fix: UserForm ts

* fix: Handle as return types, not input types

* fix: Remove profile and add avatarUrl

* fix: notFound as const

* fix: Seeder avatarUrl params

* revert: Migration changes for easier recovery

* fix: Add explicit type to builder as avatar is now set

* Add logoUrl to unpublished entity

* Avatar test out of scope here

* fix: Use the new avatarUrl after save

* chore: Removed getOrgAvatarUrl/getTeamAvatarUrl

* fix: Update sidebar image immediately after change

* Unable to safe unnamed user, so default

* fix: Unpublished page, add organization test

* Add more tests
2024-04-18 21:54:16 -07:00
5695ba7c13 feat: Tasker (#14180)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-04-18 11:56:25 -07:00
Morgan eeb0322ce1 fix: managed users check if exist (#14649) 2024-04-18 18:09:56 +00:00
MorganandLauris Skraucis 480977c2b5 fix: api v2 - platform users can disable emails and display their links (#14604)
* fix: disable emails with oauthCLient settings and hide empty links

* fixup! fix: disable emails with oauthCLient settings and hide empty links

* feat: enable to display booking success page link in emails

* fixup! feat: enable to display booking success page link in emails

* fixup! Merge branch 'fix-no-email-booking-hide-empty-links' of github.com:calcom/cal.com into fix-no-email-booking-hide-empty-links

* fixup! fixup! Merge branch 'fix-no-email-booking-hide-empty-links' of github.com:calcom/cal.com into fix-no-email-booking-hide-empty-links

* fixup! Merge branch 'main' into fix-no-email-booking-hide-empty-links

* fixup! Merge branch 'main' into fix-no-email-booking-hide-empty-links

* fixup! Merge branch 'fix-no-email-booking-hide-empty-links' of github.com:calcom/cal.com into fix-no-email-booking-hide-empty-links

---------

Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
2024-04-18 12:43:17 +00:00
Lauris Skraucis 8b2e46a6fa fix: v2 e2e tests (#14655)
* fix: oauth-clients-users e2e -> auth was changed from access token to client secret header

* fix: Gcal e2e

* remove gcal module

* simplify gcal override
2024-04-18 12:13:31 +00:00
Morgan 842be660b0 chore: atoms 1.0.30 (#14650) 2024-04-18 10:33:39 +00:00
Hariom BalharaandJoe Au-Yeung 4f69c0e502 fix: Namespaced react embeds not working with Floating and Element Click Popups (#13386)
* Fix embed queue

* Fix getCalApi namespace use

* Fixes

* Add more e2e

* Fix test

* Fix syntax error in generated code

* Make getCalApi argument optional

* Fix types

* Add more namespacing tests

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-04-18 14:41:39 +05:30
be128181e2 feat: booker atom custom location (#14635)
* frontend: handle booking locationUrl

* backend: pass booking locationUrl to handleNewBooking

* examples app: display booking location if provided

* fix: v2 not starting: contains -> includes

* backend: pass booking locationUrl to handleNewBooking

* swagger docs update

* examples app: dont display where of booking if does not start with http

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-04-18 08:26:36 +00:00
Morgan 0aa1628925 chore: atoms 1.0.29 + fix typing issues (#14633) 2024-04-18 07:30:34 +00:00
Omar López fd58f669d0 hotfix: add linting exception for website (#14645) 2024-04-17 19:51:42 +00:00
Lauris Skraucis 51927f82c0 fix: v2 app.rewrites.middleware.ts (#14626) 2024-04-17 16:48:52 +00:00
Alex van Andel 0f8a0eaf4a fix: 500 on forbidden showing up in logs (#14636) 2024-04-17 16:00:21 +00:00
b9ac22d4ed feat: AI Transcribe (#14140)
* chore: transcribe

* chore: provider

* chore: use daily provider

* chore: rebase

* chore: remove types file

* chore: remove recoil

* chore: progress

* chore: add recoil

* chore: save progress

* chore: save progress

* fix: type err

* fix: err

* fix: save progress

* feat: finish emails

* chore: add new env variable

* chore: fix type err

* chore: turbo

* chore: improvements

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-04-17 11:09:11 -04:00
ebca5c6409 fix: use 'success' value for better accuracy (#14624)
* fix: use 'success' value for better accuracy

* add success to test

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-04-17 14:01:13 +00:00
sean-brydonandUdit Takkar 6ef35e4dba chore: settings design match (#14630)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-17 09:35:18 -04:00
Carina Wollendorfer 8bb5200dd8 fix: add new styling for emoji links (#14481) 2024-04-17 12:30:14 +00:00
sean-brydon 4aeb6b7c9b fix: Hide org tab when you are team admin (#14627)
* fix: hide-tab-on-fe

* fix: backend pulling back info if the user dont have perms
2024-04-17 10:38:12 +01:00
Keith Williams ae64d7ba82 fix: API v2 recurring spelling (#14620) 2024-04-16 18:58:36 -03:00
Keith Williams 9dd2dbff19 v4.0.1 (#14618)
release.yml / release (push) Canceled after 0s
2024-04-16 21:05:18 +00:00
Joe Au-Yeung cebbffe230 fix: Minimum fix for N Calendar Invites (#14617)
* Remove pushing team member destination calendars

* Only create the event on the first destination calendar

* Only create event on the first destination calendar
2024-04-16 20:05:40 +00:00
Morgan e3e464abfa fix: booker atoms using next/router (#14612)
* fix: booker OOO using next/router

* fixup! fix: booker OOO using next/router
2024-04-16 18:37:24 +00:00
268438b0ff fix: rescheduling daily (#13736)
* fix: rescheduling daily

* fix: e2e test

* chore: test

* add location to default event type

* revert fix

* chore: add env variable

* fix: sort alphabetically

* test: checking

* chore: use another user

* chore: try adding location

* chore: undo

* chore: add variable

* chore: commented

* F

* chore: comment

* test: add unit test

* fix: type err

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2024-04-16 12:57:34 -04:00
Hariom Balhara 355c8de5e7 Set exclusive tax behaviour (#14574) 2024-04-16 13:18:08 +01:00
e6056bc4a4 fix: change max width to avoid overflow in dialog when creating new event type (#14198)
* fix: change max width to avoid overflow when creating new event dialog

* style: make max with responsive at md breakpoint

* chore: add tooltip

---------

Co-authored-by: xylonx <x956200361@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-04-16 10:48:10 +00:00
Omar López f2eebe95bd chore: Adds deprecated-imports-next-router (#14486) 2024-04-16 11:46:45 +01:00
sean-brydonandJoe Au-Yeung 3084013269 fix: always load via SSR (#14476)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-04-16 11:37:03 +01:00
68568e5a25 feat: round robin: show avatar and name of person on success page (#14245)
* feat: round robin: show avatar and name of person on success page

* fix: add background border

* fix: e2e tests

* fix: remove avatar on select, use proper border variables

* fix: bookings-list e2e round robin title

* chore: change name

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-04-16 11:36:11 +01:00
Lauris Skraucis 0eae0bb8fe refactor: remove /ee/ url segment from atoms hooks (#14602) 2024-04-16 07:29:12 -03:00
Somay Chauhan b61820d354 fix: custom attributes not being synced in intercom (#14600)
* fix: custom attributes not being synced in intercom

* fix: ssrInit
2024-04-16 11:03:45 +01:00
MorganandBailey Pumfleet 1606272850 chore: bump atoms (#14585)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2024-04-16 00:17:04 -03:00
Kaleem ea04fe1c9b fix: remove @ts-ignore (#14582) 2024-04-16 07:44:00 +05:30
Peer RichelsenandAlex van Andel 4c50b6a8af fix: hide "upgrade org banner" for isPlatform (#14522)
* not great but does the job

* comments

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-04-16 02:41:07 +01:00
sean-brydon 39aa848e9e fix: update sub-team slug - update tempOrgRedirect (#14523) 2024-04-16 01:14:41 +00:00
Peer Richelsen 64c56c1b17 Delete apps/platform directory (#14520) 2024-04-16 01:12:54 +00:00
MorganandRyukemeister 8e883a6b10 feat: update platform oauth client form (#14542)
* feat: update platform oauth client form

* fixup! feat: update platform oauth client form

* add tooltip to mention permissions cannot be modified

---------

Co-authored-by: Ryukemeister <sahalrajiv-extc@atharvacoe.ac.in>
2024-04-16 01:55:00 +01:00
Peer Richelsen 9684aa3ad5 for admins min 1 org user. for self-serve 30 (#14567) 2024-04-16 01:53:22 +01:00
sean-brydon 5fede84bf6 feat: use experimental optimizePackageImports (#14587) 2024-04-16 01:52:08 +01:00
Alex van Andel a788e07be1 fix: Re-instate hasEmailBeenChanged before sending email (#14596) 2024-04-15 22:09:06 +00:00
Somay Chauhan 68ffd2248b fix: intercom visible on mobile (#14563) 2024-04-15 21:18:42 +00:00
Carina WollendorferandCarinaWolli 4b12d021e4 remove : from email from name (#14594)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-04-15 20:27:10 +01:00
Alex van Andel 4daf9db9db fix: acme.cal.local crashes due to missing requestedSlug in seed (#14592) 2024-04-15 18:53:41 +01:00
sean-brydon a660b1eb05 fix: conditionally remove fixed prop if we're in embed (#14586)
* fix: conditionally remove fixed prop if we're in embed

* fix: conditionally remove fixed prop if we're in embed
2024-04-15 14:19:25 +01:00
Somay Chauhan 6f2f098e12 fix: removing stripe results in the event-types page loading indefintely (#14583)
* fix: removing stripe results in the event-types page loading indefinitely

* undo yarn.lock
2024-04-15 10:44:47 +00:00
Keith Williams 6a75ef86b0 v4.0.0
release.yml / release (push) Canceled after 0s
2024-04-14 20:02:28 -03:00
Alex van Andel 3d2397879c fix: Remove PageWrapper (#14579)
* fix: Remove PageWrapper

* Remove useLocale
2024-04-14 20:46:08 +00:00
Alex van Andel de474089a7 Revert "fix: Production deploys (#14578)"
This reverts commit 8b31b9aa05.
2024-04-14 16:41:25 +01:00
Keith Williams 8b31b9aa05 fix: Production deploys (#14578) 2024-04-14 15:04:30 +00:00
Crowdin Bot ad41672958 New Crowdin translations by Github Action 2024-04-14 12:51:43 +00:00
Morgan 86f2984b30 fix: only call useMe if access token is set (#14576)
* fix: only call useMe if access token is set

* chore: export use me hook
2024-04-14 12:48:30 +00:00
Morgan 38e62786e4 chore: remove /ee from apiv2 urls (#14568)
* chore: remove /ee from apiv2 urls

* fixup! chore: remove /ee from apiv2 urls

* chore: remove /ee from apiv2 urls

* fixup! chore: remove /ee from apiv2 urls

* fixup! fixup! chore: remove /ee from apiv2 urls

* fixup! fixup! fixup! chore: remove /ee from apiv2 urls
2024-04-13 22:16:37 +00:00
Keith Williams ac3d7c9fae fix: Integrate maintenance mode with current middleware (#14571)
release.yml / release (push) Canceled after 0s
* fix: API v1 maintenance mode

* Removed old middleware

* Refactor to follow same pattern as other middleware

* Added missing import
2024-04-13 20:27:22 +01:00
Rajiv Sahal 98f94ea694 return isPlaatform boolean to detect if user is a platfor user (#14570) 2024-04-13 18:42:25 +05:30
Lauris Skraucis 8ad80f7ac3 refactor: v2 event types endpoint paths (#14545)
* refactor: rename event-types/:username/public to event-types/public/:username

* refactor: move /events/public to /event-types/public/:username/:eventSlug

* fix: remove EVENT_TYPE_READ permission from public GET event-types/public/:username

* docs: add response DTO to generate docs for /public/:username/:slug aka ex. events/public

* tests: public event types endpoints

* refactor: have /public at the end of event-types routes

* correct swagger doc
2024-04-13 12:47:35 +03:00
Morgan 296678046c chore: fix class type and bump atoms version (#14562)
* chore: fix class type and bump version

* fixup! chore: fix class type and bump version

* fixup! fixup! chore: fix class type and bump version

* fixup! Merge branch 'main' into atoms-package-version-1.0.18

* fixup! fixup! Merge branch 'main' into atoms-package-version-1.0.18
2024-04-13 01:21:03 +00:00
Keith Williams fd193e651d revert: "fix: view team bookings (#14079)" (#14561)
This reverts commit f86c448359.
2024-04-13 00:53:05 +00:00
Omar LópezandKeith Williams becad5ee67 chore: adds maintenance mode for API v1 (#14555)
* Create middleware.ts

* Update middleware.ts

* Update middleware.ts

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-04-13 00:16:14 +00:00
Morgan 161dcbdd4c fix: remove prefix /api from apiv2 (#14559) 2024-04-12 23:00:12 +00:00
Crowdin Bot 7ad6652cb0 New Crowdin translations by Github Action 2024-04-12 21:52:02 +00:00
c467167e91 feat: add classNames prop to Booker atom for passing external styles (#14270)
* add classnames prop to pass external styles

* fix import

* add comments

* ignore this commit: tinkering with styling

* update typings

* mostly creating props and passing it down to child components

* remove unnecessary comment

* merge conflicts

* fixup

* fixup

* fix conflicts

* fix merge conflicts

* fix merge conflicts

* fix failing type checks

* type checks fix

* add custom classnames to booker props

* update typings

* pass in props down wherever necessary

* fixup

* still passing down around some more props

* addressing pr feedback

* final fixes

* address feedback

* fixup! address feedback

* fixup! fixup! address feedback

* fixup! fixup! fixup! address feedback

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-04-12 21:49:01 +00:00
sean-brydon 18093824d8 fix: update playwright config to allow permissions (#14526) 2024-04-12 14:13:53 -07:00
Crowdin Bot 8102f3c33c New Crowdin translations by Github Action 2024-04-12 21:09:12 +00:00
Joe Au-Yeung 902c441248 fix: Cal Video Links Expire After 14 Days (#14557)
* Set expiry date to 14 days after end time

* Add comment
2024-04-12 17:05:26 -04:00
Morgan e641bb3d57 chore: enable platform emails (#14471)
* feat: enable platform emails

* fixup! feat: enable platform emails

* fixup! fixup! feat: enable platform emails

* fixup! Merge branch 'main' into chore-platform-emails

* fixup! fixup! Merge branch 'main' into chore-platform-emails

* fixup! fixup! fixup! Merge branch 'main' into chore-platform-emails

* chore: add oauth client repo

* chore: enable platform emails using oauth client

* fix: call getConnectedCalendar with dbWrite since it creates destinationCalendar for new credentials

* chore: update doc

* fixup! chore: update doc

* fixup! fixup! chore: update doc
2024-04-12 22:39:01 +03:00
19d938ed60 feat: travel schedules to schedule timezone changes (#14512)
* add Travel Schedule modal

* UI to list schedules

* set shouldDirty

* add backend  to add new travelSchedule

* implement deleting a schedule

* check if schedule is overlapping

* WIP

* fix finding overlapping travel schedule

* only use travelSchedule when default availability is used

* adjust date overrides to timezone schedule

* first version of changeTimeZone cron job

* fix tests by adding travelSchedules

* fixes for cron job api call

* improve unit tests

* add migration

* fix type error

* fix collective-scheduling test

* clean up cron job

* code clean up

* code clean up

* show timezone from travel schedule for date override

* add date override tests

* show tz on date override only in default schedule

* fix deleting old schedules

* minor fixes in cron api handler

* code clean up

* code clean up from feedback

* fix asia/kalkota comment

* fix dark mode

* fix start and end date conversion to utc

* add first unit test for travel schedules

* Fix modal render issue

* show timezone city wtihout _

* fix dark more for datepicker

* reset values after closing dialog

* remove session from middleware

* exit loop early once schedule is found

* fix type error

* add getTravelSchedules handler

* clean up DatePicker

* fix type error

* code clean up

* code clean up

* add indexes

* use deleteMany

* fix icons

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-04-12 17:57:28 +00:00
5561949f9b feat: Delete/Create WebhookScheduledTriggers for existing bookings (#14121)
* feat: Delete/Create WebhookScheduledTriggers for existing bookings

* fix: type check

* fix: early return, query status in prisma, unnecessary trigger type check

* feat: add/delete scheduled webhook triggers for team and user

---------

Co-authored-by: v0ltZzie <samit091848@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-12 16:57:59 +00:00
Lauris Skraucis 5e98cca062 fix: v2 create schedule with availabilities (#14551)
* fix: v2 schedule with availabilities creation

* driveby: remove permissions from public event-types endpoint
2024-04-12 15:49:47 +00:00
ac858c9bee fix: [CAL-3374] Says Number is not Verified, but it is Verified (#14531)
* fix: [CAL-3374] Says Number is not Verified, but it is Verified

* clearing error only when verified

* type-checks fixed

* chore: improvement

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-04-12 11:33:24 -04:00
Erik f58f5e4e02 chore: SDK version 1.0.1 (#14524) 2024-04-12 15:31:04 +00:00
Alex van Andel ad844f5e51 fix: Remove hardcoded stripe.. (#14549) 2024-04-12 15:50:02 +01:00
Morgan 53c1bc45d6 fix: atoms package types were not exported (#14546) 2024-04-12 13:16:07 +00:00
Hariom Balhara 0cc93aa87e chore: Publish new versions of embeds (#14543) 2024-04-12 09:44:37 +01:00
Amit Sharma b41b830086 fix: State Inconsistency issue on webhook edit modal (#14544) 2024-04-12 14:08:58 +05:30
476 changed files with 8086 additions and 3387 deletions
+1
View File
@@ -38,6 +38,7 @@ BASECAMP3_USER_AGENT=
DAILY_API_KEY=
DAILY_SCALE_PLAN=''
DAILY_WEBHOOK_SECRET=''
DAILY_MEETING_ENDED_WEBHOOK_SECRET=''
# - GOOGLE CALENDAR/MEET/LOGIN
# Needed to enable Google Calendar integration and Login with Google
+5 -1
View File
@@ -240,7 +240,7 @@ CSP_POLICY=
# Vercel Edge Config
EDGE_CONFIG=
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD=0 # Override the booker to only load X number of days worth of data
# Control time intervals on a user's Schedule availability
@@ -347,6 +347,10 @@ SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
# api v2
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
# Tasker features
TASKER_ENABLE_WEBHOOKS=0
TASKER_ENABLE_EMAILS=0
# Ratelimiting via unkey
UNKEY_ROOT_KEY=
@@ -35,6 +35,7 @@ env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
jobs:
build:
+24
View File
@@ -0,0 +1,24 @@
name: Cron - changeTimeZone
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs "At every full hour." (see https://crontab.guru)
- cron: "0 * * * *"
jobs:
cron-scheduleEmailReminders:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_API_KEY }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/changeTimeZone \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
-sSf
+2
View File
@@ -62,6 +62,7 @@ jobs:
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
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 }}
@@ -91,6 +92,7 @@ jobs:
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
+2
View File
@@ -49,6 +49,7 @@ jobs:
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
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 }}
@@ -73,6 +74,7 @@ jobs:
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
+2
View File
@@ -55,6 +55,7 @@ jobs:
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
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 }}
@@ -80,6 +81,7 @@ jobs:
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
+2
View File
@@ -53,6 +53,7 @@ jobs:
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
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 }}
@@ -82,6 +83,7 @@ jobs:
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
@@ -6,6 +6,7 @@ on:
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
@@ -31,6 +32,7 @@ env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
jobs:
+2
View File
@@ -6,6 +6,7 @@ on:
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
@@ -35,6 +36,7 @@ env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
jobs:
@@ -0,0 +1,23 @@
import { get } from "@vercel/edge-config";
import type { NextMiddleware } from "next-api-middleware";
const safeGet = async <T = unknown>(key: string): Promise<T | undefined> => {
try {
return get<T>(key);
} catch (error) {
// Don't crash if EDGE_CONFIG env var is missing
}
};
export const config = { matcher: "/:path*" };
export const checkIsInMaintenanceMode: NextMiddleware = async (req, res, next) => {
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
if (isInMaintenanceMode) {
return res
.status(503)
.json({ message: "API is currently under maintenance. Please try again at a later time." });
}
await next();
};
@@ -2,6 +2,7 @@ import { label } from "next-api-middleware";
import { addRequestId } from "./addRequestid";
import { captureErrors } from "./captureErrors";
import { checkIsInMaintenanceMode } from "./checkIsInMaintenanceMode";
import { extendRequest } from "./extendRequest";
import {
HTTP_POST,
@@ -24,6 +25,7 @@ const middleware = {
HTTP_POST,
HTTP_DELETE,
addRequestId,
checkIsInMaintenanceMode,
verifyApiKey,
rateLimitApiKey,
extendRequest,
@@ -36,6 +38,7 @@ type Middleware = keyof typeof middleware;
const middlewareOrder = [
// The order here, determines the order of execution
"checkIsInMaintenanceMode",
"extendRequest",
"captureErrors",
"verifyApiKey",
-2
View File
@@ -82,7 +82,6 @@ export const schemaUserBaseBodyParams = User.pick({
brandColor: true,
darkBrandColor: true,
allowDynamicBooking: true,
away: true,
role: true,
// @note: disallowing avatar changes via API for now. We can add it later if needed. User should upload image via UI.
// avatar: true,
@@ -171,7 +170,6 @@ export const schemaUserReadPublic = User.pick({
brandColor: true,
darkBrandColor: true,
allowDynamicBooking: true,
away: true,
createdDate: true,
verified: true,
invitedTo: true,
+1 -1
View File
@@ -7,7 +7,7 @@ import { createMocks } from "node-mocks-http";
import { describe, expect, test, vi } from "vitest";
import dayjs from "@calcom/dayjs";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
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";
+5
View File
@@ -1,4 +1,5 @@
import { AppLoggerMiddleware } from "@/app.logger.middleware";
import { RewriterMiddleware } from "@/app.rewrites.middleware";
import appConfig from "@/config/app";
import { AuthModule } from "@/modules/auth/auth.module";
import { EndpointsModule } from "@/modules/endpoints.module";
@@ -6,6 +7,7 @@ import { JwtModule } from "@/modules/jwt/jwt.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { RouterModule } from "@nestjs/core";
import { AppController } from "./app.controller";
@@ -34,11 +36,14 @@ import { AppController } from "./app.controller";
EndpointsModule,
AuthModule,
JwtModule,
//register prefix for all routes in EndpointsModule
RouterModule.register([{ path: "/v2", module: EndpointsModule }]),
],
controllers: [AppController],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer.apply(AppLoggerMiddleware).forRoutes("*");
consumer.apply(RewriterMiddleware).forRoutes("/");
}
}
@@ -0,0 +1,18 @@
import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response } from "express";
@Injectable()
export class RewriterMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
if (req.url.startsWith("/api/v2")) {
req.url = req.url.replace("/api/v2", "/v2");
}
if (req.url.startsWith("/v2/ee")) {
req.url = req.url.replace("/v2/ee", "/v2");
}
if (req.url.includes("reccuring")) {
req.url = req.url.replace("reccuring", "recurring");
}
next();
}
}
+3 -5
View File
@@ -11,6 +11,8 @@ import * as Sentry from "@sentry/node";
import * as cookieParser from "cookie-parser";
import helmet from "helmet";
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
@@ -26,7 +28,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
app.enableCors({
origin: "*",
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
allowedHeaders: ["Accept", "Authorization", "Content-Type", "Origin"],
allowedHeaders: [X_CAL_CLIENT_ID, X_CAL_SECRET_KEY, "Accept", "Authorization", "Content-Type", "Origin"],
maxAge: 86_400,
});
@@ -58,10 +60,6 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalFilters(new TRPCExceptionFilter());
app.setGlobalPrefix("api", {
exclude: [{ path: "health", method: RequestMethod.GET }],
});
app.use(cookieParser());
return app;
+1 -1
View File
@@ -14,7 +14,7 @@ const loadConfig = (): AppConfig => {
process.env.API_PORT && getEnv("NODE_ENV", "development") === "development"
? `:${Number(getEnv("API_PORT", "5555"))}`
: ""
}/api/v2`,
}/v2`,
},
db: {
readUrl: getEnv("DATABASE_READ_URL"),
@@ -1,4 +1,5 @@
import { BookingsController } from "@/ee/bookings/controllers/bookings.controller";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
@@ -7,7 +8,7 @@ import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, TokensModule],
providers: [TokensRepository, OAuthFlowService],
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository],
controllers: [BookingsController],
})
export class BookingsModule {}
@@ -115,7 +115,7 @@ describe("Bookings Endpoints", () => {
};
return request(app.getHttpServer())
.post("/api/v2/ee/bookings")
.post("/v2/bookings")
.send(body)
.expect(201)
.then(async (response) => {
@@ -138,7 +138,7 @@ describe("Bookings Endpoints", () => {
it("should get bookings", async () => {
return request(app.getHttpServer())
.get("/api/v2/ee/bookings?filters[status]=upcoming")
.get("/v2/bookings?filters[status]=upcoming")
.then((response) => {
console.log("asap responseBody", JSON.stringify(response.body, null, 2));
const responseBody: GetBookingsOutput = response.body;
@@ -159,7 +159,7 @@ describe("Bookings Endpoints", () => {
it("should get booking", async () => {
return request(app.getHttpServer())
.get(`/api/v2/ee/bookings/${createdBooking.uid}`)
.get(`/v2/bookings/${createdBooking.uid}`)
.then((response) => {
const responseBody: GetBookingOutput = response.body;
const bookingInfo = responseBody.data;
@@ -199,7 +199,7 @@ describe("Bookings Endpoints", () => {
// };
// return request(app.getHttpServer())
// .post("/api/v2/ee/bookings/reccuring")
// .post("/v2/bookings/recurring")
// .send(body)
// .expect(201)
// .then((response) => {
@@ -231,7 +231,7 @@ describe("Bookings Endpoints", () => {
// };
// return request(app.getHttpServer())
// .post("/api/v2/ee/bookings/instant")
// .post("/v2/bookings/instant")
// .send(body)
// .expect(201)
// .then((response) => {
@@ -250,7 +250,7 @@ describe("Bookings Endpoints", () => {
};
return request(app.getHttpServer())
.post(`/api/v2/ee/bookings/${bookingId}/cancel`)
.post(`/v2/bookings/${bookingId}/cancel`)
.send(body)
.expect(201)
.then((response) => {
@@ -1,11 +1,12 @@
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
import { CreateReccuringBookingInput } from "@/ee/bookings/inputs/create-reccuring-booking.input";
import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input";
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import {
@@ -15,6 +16,7 @@ import {
Req,
InternalServerErrorException,
Body,
Headers,
HttpException,
Param,
Get,
@@ -27,6 +29,7 @@ import { User } from "@prisma/client";
import { Request } from "express";
import { NextApiRequest } from "next/types";
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
import { BOOKING_READ, SUCCESS_STATUS } from "@calcom/platform-constants";
import {
getAllUserBookings,
@@ -45,8 +48,30 @@ import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-t
import { ApiResponse } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
type BookingRequest = Request & {
userId?: number;
};
type OAuthRequestParams = {
platformClientId: string;
platformRescheduleUrl: string;
platformCancelUrl: string;
platformBookingUrl: string;
platformBookingLocation?: string;
arePlatformEmailsEnabled: boolean;
};
const DEFAULT_PLATFORM_PARAMS = {
platformClientId: "",
platformCancelUrl: "",
platformRescheduleUrl: "",
platformBookingUrl: "",
arePlatformEmailsEnabled: false,
platformBookingLocation: undefined,
};
@Controller({
path: "ee/bookings",
path: "/bookings",
version: "2",
})
@UseGuards(PermissionsGuard)
@@ -56,7 +81,8 @@ export class BookingsController {
constructor(
private readonly oAuthFlowService: OAuthFlowService,
private readonly prismaReadService: PrismaReadService
private readonly prismaReadService: PrismaReadService,
private readonly oAuthClientRepository: OAuthClientRepository
) {}
@Get("/")
@@ -117,13 +143,16 @@ export class BookingsController {
@Post("/")
async createBooking(
@Req() req: Request & { userId?: number },
@Body() _: CreateBookingInput
@Req() req: BookingRequest,
@Body() body: CreateBookingInput,
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse<unknown>> {
req.userId = (await this.getOwnerId(req)) ?? -1;
req.body = { ...req.body, noEmail: true };
const oAuthClientId = clientId?.toString();
const locationUrl = body.locationUrl;
try {
const booking = await handleNewBooking(req as unknown as NextApiRequest & { userId?: number });
const booking = await handleNewBooking(
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
);
return {
status: SUCCESS_STATUS,
data: booking,
@@ -136,15 +165,16 @@ export class BookingsController {
@Post("/:bookingId/cancel")
async cancelBooking(
@Req() req: Request & { userId?: number },
@Req() req: BookingRequest,
@Param("bookingId") bookingId: string,
@Body() body: CancelBookingInput
@Body() _: CancelBookingInput,
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse> {
const oAuthClientId = clientId?.toString();
if (bookingId) {
req.userId = (await this.getOwnerId(req)) ?? -1;
req.body = { ...body, id: parseInt(bookingId) };
try {
await handleCancelBooking(req as unknown as NextApiRequest & { userId?: number });
req.body.id = parseInt(bookingId);
await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
return {
status: SUCCESS_STATUS,
};
@@ -157,16 +187,16 @@ export class BookingsController {
throw new InternalServerErrorException("Could not cancel booking.");
}
@Post("/reccuring")
async createReccuringBooking(
@Req() req: Request & { userId?: number },
@Body() _: CreateReccuringBookingInput[]
@Post("/recurring")
async createRecurringBooking(
@Req() req: BookingRequest,
@Body() _: CreateRecurringBookingInput[],
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse<BookingResponse[]>> {
req.userId = (await this.getOwnerId(req)) ?? -1;
req.body = { ...req.body, noEmail: true };
const oAuthClientId = clientId?.toString();
try {
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
req as unknown as NextApiRequest & { userId?: number }
await this.createNextApiBookingRequest(req, oAuthClientId)
);
return {
status: SUCCESS_STATUS,
@@ -180,14 +210,15 @@ export class BookingsController {
@Post("/instant")
async createInstantBooking(
@Req() req: Request & { userId?: number },
@Body() _: CreateBookingInput
@Req() req: BookingRequest,
@Body() _: CreateBookingInput,
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
const oAuthClientId = clientId?.toString();
req.userId = (await this.getOwnerId(req)) ?? -1;
req.body = { ...req.body, noEmail: true };
try {
const instantMeeting = await handleInstantMeeting(
req as unknown as NextApiRequest & { userId?: number }
await this.createNextApiBookingRequest(req, oAuthClientId)
);
return {
status: SUCCESS_STATUS,
@@ -209,6 +240,39 @@ export class BookingsController {
this.logger.error(err);
}
}
async getOAuthClientsParams(req: BookingRequest, clientId: string): Promise<OAuthRequestParams> {
const res = DEFAULT_PLATFORM_PARAMS;
try {
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
// fetch oAuthClient from db and use data stored in db to set these values
if (client) {
res.platformClientId = clientId;
res.platformCancelUrl = client.bookingCancelRedirectUri ?? "";
res.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
res.platformBookingUrl = client.bookingRedirectUri ?? "";
res.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false;
}
return res;
} catch (err) {
this.logger.error(err);
return res;
}
}
async createNextApiBookingRequest(
req: BookingRequest,
oAuthClientId?: string,
platformBookingLocation?: string
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
const userId = (await this.getOwnerId(req)) ?? -1;
const oAuthParams = oAuthClientId
? await this.getOAuthClientsParams(req, oAuthClientId)
: DEFAULT_PLATFORM_PARAMS;
Object.assign(req, { userId, ...oAuthParams, platformBookingLocation });
req.body = { ...req.body, noEmail: !oAuthParams.arePlatformEmailsEnabled };
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
}
function handleBookingErrors(err: Error | HttpError | unknown, type?: "recurring" | `instant`): void {
@@ -98,4 +98,8 @@ export class CreateBookingInput {
@Type(() => Response)
responses!: Response;
@IsString()
@IsOptional()
locationUrl?: string;
}
@@ -3,7 +3,7 @@ import { IsBoolean, IsNumber, IsOptional } from "class-validator";
import type { AppsStatus } from "@calcom/platform-libraries";
export class CreateReccuringBookingInput extends CreateBookingInput {
export class CreateRecurringBookingInput extends CreateBookingInput {
@IsBoolean()
@IsOptional()
noEmail?: boolean;
@@ -11,7 +11,7 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { CalendarBusyTimesInput } from "@calcom/platform-types";
@Controller({
path: "ee/calendars",
path: "/calendars",
version: "2",
})
@UseGuards(AccessTokenGuard)
@@ -3,6 +3,7 @@ import {
CredentialsWithUserEmail,
} from "@/modules/credentials/credentials.repository";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UsersRepository } from "@/modules/users/users.repository";
import {
Injectable,
@@ -23,7 +24,8 @@ export class CalendarsService {
constructor(
private readonly usersRepository: UsersRepository,
private readonly credentialsRepository: CredentialsRepository,
private readonly dbRead: PrismaReadService
private readonly dbRead: PrismaReadService,
private readonly dbWrite: PrismaWriteService
) {}
async getCalendars(userId: number) {
@@ -35,7 +37,7 @@ export class CalendarsService {
return getConnectedDestinationCalendars(
userWithCalendars,
false,
this.dbRead.prisma as unknown as PrismaClient
this.dbWrite.prisma as unknown as PrismaClient
);
}
@@ -3,7 +3,9 @@ import { AppModule } from "@/app.module";
import { EventTypesModule } from "@/ee/event-types/event-types.module";
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
@@ -64,7 +66,7 @@ describe("Event types Endpoints", () => {
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
const userEmail = "event-types-test-e2e@api.com";
const name = "bob the builder";
const name = "bob-the-builder";
const username = name;
let eventType: EventType;
let user: User;
@@ -183,6 +185,41 @@ describe("Event types Endpoints", () => {
expect(responseBody.data.eventType.userId).toEqual(user.id);
});
it(`/GET/:username/public`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${username}/public`)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: GetEventTypesPublicOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(1);
expect(responseBody.data?.[0]?.id).toEqual(eventType.id);
expect(responseBody.data?.[0]?.title).toEqual(eventType.title);
expect(responseBody.data?.[0]?.slug).toEqual(eventType.slug);
expect(responseBody.data?.[0]?.length).toEqual(eventType.length);
});
it(`/GET/:username/:eventSlug/public`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${username}/${eventType.slug}/public`)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: GetEventTypePublicOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.id).toEqual(eventType.id);
expect(responseBody.data?.title).toEqual(eventType.title);
expect(responseBody.data?.slug).toEqual(eventType.slug);
expect(responseBody.data?.length).toEqual(eventType.length);
});
it(`/GET/`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types`)
@@ -202,7 +239,7 @@ describe("Event types Endpoints", () => {
expect(responseBody.data.profiles?.[0]?.name).toEqual(name);
});
it(`/GET/:username/public`, async () => {
it(`/GET/public/:username/`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${username}/public`)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
@@ -1,7 +1,9 @@
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
import { GetPublicEventTypeQueryParams } from "@/ee/event-types/inputs/get-public-event-type-query-params.input";
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
import { DeleteEventTypeOutput } from "@/ee/event-types/outputs/delete-event-type.output";
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
import { GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
@@ -11,6 +13,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
@@ -24,11 +27,15 @@ import {
HttpCode,
HttpStatus,
Delete,
Query,
InternalServerErrorException,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { getPublicEvent } from "@calcom/platform-libraries";
import { getEventTypesByViewer } from "@calcom/platform-libraries";
import { PrismaClient } from "@calcom/prisma";
@Controller({
path: "event-types",
@@ -37,7 +44,10 @@ import { getEventTypesByViewer } from "@calcom/platform-libraries";
@UseGuards(PermissionsGuard)
@DocsTags("Event types")
export class EventTypesController {
constructor(private readonly eventTypesService: EventTypesService) {}
constructor(
private readonly eventTypesService: EventTypesService,
private readonly prismaReadService: PrismaReadService
) {}
@Post("/")
@Permissions([EVENT_TYPE_WRITE])
@@ -90,8 +100,36 @@ export class EventTypesController {
};
}
@Get("/:username/:eventSlug/public")
async getPublicEventType(
@Param("username") username: string,
@Param("eventSlug") eventSlug: string,
@Query() queryParams: GetPublicEventTypeQueryParams
): Promise<GetEventTypePublicOutput> {
try {
const event = await getPublicEvent(
username.toLowerCase(),
eventSlug,
queryParams.isTeamEvent,
queryParams.org || null,
this.prismaReadService.prisma as unknown as PrismaClient,
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
true
);
return {
data: event,
status: SUCCESS_STATUS,
};
} catch (err) {
if (err instanceof Error) {
throw new NotFoundException(err.message);
}
}
throw new InternalServerErrorException("Could not find public event.");
}
@Get("/:username/public")
@Permissions([EVENT_TYPE_READ])
async getPublicEventTypes(@Param("username") username: string): Promise<GetEventTypesPublicOutput> {
const eventTypes = await this.eventTypesService.getEventTypesPublicByUsername(username);
@@ -1,5 +1,6 @@
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import type { PrismaClient } from "@calcom/prisma";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
@@ -48,9 +49,7 @@ export class EventTypesRepository {
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
eventTypeId,
userId: user.id,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prisma: this.dbRead.prisma,
prisma: this.dbRead.prisma as unknown as PrismaClient,
isUserOrganizationAdmin,
isTrpcCall: true,
});
@@ -2,16 +2,7 @@ import { ApiProperty } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsBoolean, IsOptional, IsString } from "class-validator";
export class GetPublicEventInput {
@IsString()
@Transform(({ value }: { value: string }) => value.toLowerCase())
@ApiProperty({ required: true })
username!: string;
@IsString()
@ApiProperty({ required: true })
eventSlug!: string;
export class GetPublicEventTypeQueryParams {
@Transform(({ value }: { value: string }) => value === "true")
@IsBoolean()
@IsOptional()
@@ -0,0 +1,355 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsBoolean,
IsInt,
IsOptional,
IsString,
IsUrl,
ValidateNested,
IsArray,
IsObject,
IsNumber,
IsEnum,
} from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Location {
@IsString()
type!: string;
}
class Source {
@IsString()
id!: string;
@IsString()
type!: string;
@IsString()
label!: string;
}
class OptionInput {
@IsString()
type!: string;
@IsBoolean()
@IsOptional()
required?: boolean;
@IsString()
@IsOptional()
placeholder?: string;
}
class BookingField {
@IsString()
name!: string;
@IsString()
type!: string;
@IsOptional()
@IsString()
defaultLabel?: string;
@IsString()
@IsOptional()
label?: string;
@IsString()
@IsOptional()
placeholder?: string;
@IsBoolean()
@IsOptional()
required?: boolean;
@IsOptional()
getOptionsAt?: string;
@IsObject()
@IsOptional()
optionsInputs?: { [key: string]: OptionInput };
@IsBoolean()
@IsOptional()
hideWhenJustOneOption?: boolean;
@IsString()
@IsOptional()
editable?: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => Source)
@IsOptional()
sources?: Source[];
}
class Organization {
@IsInt()
id!: number;
@IsString()
@IsOptional()
slug?: string | null;
@IsString()
name!: string;
@IsOptional()
metadata!: Record<string, any>;
}
class Profile {
@IsString()
username!: string | null;
@IsInt()
id!: number | null;
@IsInt()
@IsOptional()
userId?: number;
@IsString()
@IsOptional()
uid?: string;
@IsOptional()
@IsString()
name?: string;
@IsInt()
organizationId!: number | null;
@ValidateNested()
@Type(() => Organization)
organization?: Organization | null;
@IsString()
upId!: string;
@IsString()
@IsOptional()
image?: string;
@IsString()
@IsOptional()
brandColor?: string;
@IsString()
@IsOptional()
darkBrandColor?: string;
@IsString()
@IsOptional()
theme?: string;
@IsOptional()
bookerLayouts?: any;
}
class Owner {
@IsInt()
id!: number;
@IsString()
@IsOptional()
avatarUrl?: string | null;
@IsString()
username!: string | null;
@IsString()
name!: string | null;
@IsString()
weekStart!: string;
@IsString()
@IsOptional()
brandColor?: string | null;
@IsString()
@IsOptional()
darkBrandColor?: string | null;
@IsString()
@IsOptional()
theme?: string | null;
@IsOptional()
metadata!: any;
@IsInt()
@IsOptional()
defaultScheduleId?: number | null;
@IsString()
nonProfileUsername!: string | null;
@ValidateNested()
@Type(() => Profile)
profile!: Profile;
}
class User {
@IsString()
username!: string | null;
@IsString()
name!: string | null;
@IsString()
weekStart!: string;
@IsInt()
organizationId?: number;
@IsString()
@IsOptional()
avatarUrl?: string | null;
@ValidateNested()
profile!: Profile;
@IsString()
bookerUrl!: string;
}
class Schedule {
@IsInt()
id!: number;
@IsString()
timeZone!: string | null;
}
class PublicEventTypeOutput {
@IsInt()
id!: number;
@IsString()
title!: string;
@IsString()
description!: string;
@IsString()
@IsOptional()
eventName?: string | null;
@IsString()
slug!: string;
@IsBoolean()
isInstantEvent!: boolean;
@IsOptional()
aiPhoneCallConfig?: any;
@IsOptional()
schedulingType?: any;
@IsInt()
length!: number;
@IsArray()
@ValidateNested({ each: true })
@Type(() => Location)
locations!: Location[];
@IsArray()
customInputs!: any[];
@IsBoolean()
disableGuests!: boolean;
@IsObject()
metadata!: object | null;
@IsBoolean()
lockTimeZoneToggleOnBookingPage!: boolean;
@IsBoolean()
requiresConfirmation!: boolean;
@IsBoolean()
requiresBookerEmailVerification!: boolean;
@IsOptional()
recurringEvent?: any;
@IsNumber()
price!: number;
@IsString()
currency!: string;
@IsOptional()
seatsPerTimeSlot?: number | null;
@IsBoolean()
seatsShowAvailabilityCount!: boolean | null;
@IsArray()
@ValidateNested({ each: true })
@Type(() => BookingField)
bookingFields!: BookingField[];
@IsOptional()
team?: any;
@IsOptional()
@IsUrl()
successRedirectUrl?: string | null;
@IsArray()
workflows!: any[];
@IsArray()
hosts!: any[];
@ValidateNested()
@Type(() => Owner)
owner!: Owner | null;
@ValidateNested()
@Type(() => Schedule)
schedule!: Schedule | null;
@IsBoolean()
hidden!: boolean;
@IsBoolean()
assignAllTeamMembers!: boolean;
@IsOptional()
bookerLayouts?: any;
@IsArray()
@ValidateNested({ each: true })
@Type(() => User)
users!: User[];
@IsObject()
entity!: object;
@IsBoolean()
isDynamic!: boolean;
}
export class GetEventTypePublicOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ValidateNested({ each: true })
@Type(() => PublicEventTypeOutput)
@IsArray()
data!: PublicEventTypeOutput | null;
}
@@ -1,5 +1,6 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
@@ -18,6 +19,20 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
const CLIENT_REDIRECT_URI = "http://localhost:5555";
class CalendarsServiceMock {
async getCalendars() {
return {
connectedCalendars: [
{
integration: {
type: "google_calendar",
},
},
],
};
}
}
describe("Platform Gcal Endpoints", () => {
let app: INestApplication;
@@ -42,6 +57,8 @@ describe("Platform Gcal Endpoints", () => {
.useValue({
canActivate: () => true,
})
.overrideProvider(CalendarsService)
.useClass(CalendarsServiceMock)
.compile();
app = moduleRef.createNestApplication();
@@ -85,14 +102,14 @@ describe("Platform Gcal Endpoints", () => {
it(`/GET/ee/gcal/oauth/auth-url: it should respond 401 with invalid access token`, async () => {
await request(app.getHttpServer())
.get(`/api/v2/ee/gcal/oauth/auth-url`)
.get(`/v2/gcal/oauth/auth-url`)
.set("Authorization", `Bearer invalid_access_token`)
.expect(401);
});
it(`/GET/ee/gcal/oauth/auth-url: it should auth-url to google oauth with valid access token `, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/ee/gcal/oauth/auth-url`)
.get(`/v2/gcal/oauth/auth-url`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.set("Origin", CLIENT_REDIRECT_URI)
.expect(200);
@@ -103,7 +120,7 @@ describe("Platform Gcal Endpoints", () => {
it(`/GET/ee/gcal/oauth/save: without oauth code`, async () => {
await request(app.getHttpServer())
.get(
`/api/v2/ee/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3D${CLIENT_REDIRECT_URI}&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
`/v2/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3D${CLIENT_REDIRECT_URI}&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
)
.expect(400);
});
@@ -111,7 +128,7 @@ describe("Platform Gcal Endpoints", () => {
it(`/GET/ee/gcal/oauth/save: without access token`, async () => {
await request(app.getHttpServer())
.get(
`/api/v2/ee/gcal/oauth/save?state=origin%3D${CLIENT_REDIRECT_URI}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
`/v2/gcal/oauth/save?state=origin%3D${CLIENT_REDIRECT_URI}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
)
.expect(400);
});
@@ -119,25 +136,25 @@ describe("Platform Gcal Endpoints", () => {
it(`/GET/ee/gcal/oauth/save: without origin`, async () => {
await request(app.getHttpServer())
.get(
`/api/v2/ee/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
`/v2/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
)
.expect(400);
});
it(`/GET/ee/gcal/check with access token but without origin`, async () => {
await request(app.getHttpServer())
.get(`/api/v2/ee/gcal/check`)
.get(`/v2/gcal/check`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.expect(400);
});
it(`/GET/ee/gcal/check without access token`, async () => {
await request(app.getHttpServer()).get(`/api/v2/ee/gcal/check`).expect(401);
await request(app.getHttpServer()).get(`/v2/gcal/check`).expect(401);
});
it(`/GET/ee/gcal/check with access token and origin but no credentials`, async () => {
await request(app.getHttpServer())
.get(`/api/v2/ee/gcal/check`)
.get(`/v2/gcal/check`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.set("Origin", CLIENT_REDIRECT_URI)
.expect(400);
@@ -151,7 +168,7 @@ describe("Platform Gcal Endpoints", () => {
"google-calendar"
);
await request(app.getHttpServer())
.get(`/api/v2/ee/gcal/check`)
.get(`/v2/gcal/check`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.set("Origin", CLIENT_REDIRECT_URI)
.expect(200);
+2 -2
View File
@@ -44,7 +44,7 @@ const CALENDAR_SCOPES = [
// Controller for the GCalConnect Atom
@Controller({
path: "ee/gcal",
path: "/gcal",
version: "2",
})
@DocsTags("Google Calendar")
@@ -60,7 +60,7 @@ export class GcalController {
private readonly calendarsService: CalendarsService
) {}
private redirectUri = `${this.config.get("api.url")}/ee/gcal/oauth/save`;
private redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
@Get("/oauth/auth-url")
@HttpCode(HttpStatus.OK)
@@ -71,7 +71,7 @@ describe("Me Endpoints", () => {
it("should get user associated with access token", async () => {
return request(app.getHttpServer())
.get("/api/v2/ee/me")
.get("/v2/me")
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<UserResponse> = response.body;
@@ -90,7 +90,7 @@ describe("Me Endpoints", () => {
const body: UpdateManagedUserInput = { timeZone: "Europe/Rome" };
return request(app.getHttpServer())
.patch("/api/v2/ee/me")
.patch("/v2/me")
.send(body)
.expect(200)
.then(async (response) => {
@@ -114,22 +114,19 @@ describe("Me Endpoints", () => {
it("should not update user associated with access token given invalid timezone", async () => {
const bodyWithIncorrectTimeZone: UpdateManagedUserInput = { timeZone: "Narnia/Woods" };
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectTimeZone).expect(400);
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeZone).expect(400);
});
it("should not update user associated with access token given invalid time format", async () => {
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 };
return request(app.getHttpServer())
.patch("/api/v2/ee/me")
.send(bodyWithIncorrectTimeFormat)
.expect(400);
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
});
it("should not update user associated with access token given invalid week start", async () => {
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" };
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectWeekStart).expect(400);
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
});
afterAll(async () => {
+1 -1
View File
@@ -14,7 +14,7 @@ import { PROFILE_READ, PROFILE_WRITE, SUCCESS_STATUS } from "@calcom/platform-co
import { userSchemaResponse } from "@calcom/platform-types";
@Controller({
path: "ee/me",
path: "/me",
version: "2",
})
@UseGuards(AccessTokenGuard, PermissionsGuard)
@@ -20,7 +20,7 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@Controller({
path: "ee/provider",
path: "/provider",
version: "2",
})
@DocsTags("Cal provider")
@@ -19,12 +19,17 @@ export class CreateAvailabilityInput {
}
function transformStringToDate(value: string, key: string): Date {
const parts = value.split(":");
// note(Lauris): incoming value is ISO8061 e.g. 2025-0412T13:17:56.324Z
const dateTimeParts = value.split("T");
const timePart = dateTimeParts[1].split(".")[0]; // Removes milliseconds
const parts = timePart.split(":");
if (parts.length !== 3) {
throw new BadRequestException(`Invalid ${key} format. Expected format: HH:MM:SS. Received ${value}`);
throw new BadRequestException(
`Invalid time format. Expected format(ISO8061): 2025-0412T13:17:56.324Z. Received: ${value}`
);
}
const [hours, minutes, seconds] = parts.map(Number);
if (hours < 0 || hours > 23) {
@@ -27,7 +27,7 @@ export class CredentialsRepository {
invalid: false,
},
where: {
id: credential?.id,
id: credential?.id ?? 0,
},
});
}
+1 -2
View File
@@ -1,12 +1,11 @@
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
import { EventsModule } from "@/modules/events/events.module";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { TimezoneModule } from "@/modules/timezones/timezones.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
import { Module } from "@nestjs/common";
@Module({
imports: [EventsModule, OAuthClientModule, PlatformEndpointsModule, TimezoneModule],
imports: [OAuthClientModule, PlatformEndpointsModule, TimezoneModule],
})
export class EndpointsModule implements NestModule {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -1,43 +0,0 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Controller, Get, NotFoundException, InternalServerErrorException, Query } from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { getPublicEvent } from "@calcom/platform-libraries";
import type { PublicEventType } from "@calcom/platform-libraries";
import { ApiResponse, GetPublicEventInput } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
@Controller({
path: "events",
version: "2",
})
@DocsTags("Event types")
export class EventsController {
constructor(private readonly prismaReadService: PrismaReadService) {}
@Get("/public")
async getPublicEvent(@Query() queryParams: GetPublicEventInput): Promise<ApiResponse<PublicEventType>> {
try {
const event = await getPublicEvent(
queryParams.username,
queryParams.eventSlug,
queryParams.isTeamEvent,
queryParams.org || null,
this.prismaReadService.prisma as unknown as PrismaClient,
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
true
);
return {
data: event,
status: SUCCESS_STATUS,
};
} catch (err) {
if (err instanceof Error) {
throw new NotFoundException(err.message);
}
}
throw new InternalServerErrorException("Could not find public event.");
}
}
@@ -1,9 +0,0 @@
import { EventsController } from "@/modules/events/controllers/events.controller";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
controllers: [EventsController],
})
export class EventsModule {}
@@ -199,7 +199,7 @@ describe("OAuth Client Users Endpoints", () => {
it(`/GET/:id`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
.set("x-cal-secret-key", oAuthClient.secret)
.set("Origin", `${CLIENT_REDIRECT_URI}`)
.expect(200);
@@ -216,7 +216,7 @@ describe("OAuth Client Users Endpoints", () => {
const response = await request(app.getHttpServer())
.patch(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
.set("x-cal-secret-key", oAuthClient.secret)
.set("Origin", `${CLIENT_REDIRECT_URI}`)
.send(body)
.expect(200);
@@ -231,7 +231,7 @@ describe("OAuth Client Users Endpoints", () => {
it(`/DELETE/:id`, () => {
return request(app.getHttpServer())
.delete(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
.set("x-cal-secret-key", oAuthClient.secret)
.set("Origin", `${CLIENT_REDIRECT_URI}`)
.expect(200);
});
@@ -76,11 +76,6 @@ export class OAuthClientUsersController {
this.logger.log(
`Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}`
);
const existingUser = await this.userRepository.findByEmail(body.email);
if (existingUser) {
throw new BadRequestException("A user with the provided email already exists.");
}
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
const isPlatformManaged = true;
@@ -23,7 +23,7 @@ export class OAuthClientUsersService {
isPlatformManaged: boolean,
organizationId?: number
) {
const existsWithEmail = await this.userExistsWithEmail(oAuthClientId, body.email);
const existsWithEmail = await this.managedUserExistsWithEmail(oAuthClientId, body.email);
if (existsWithEmail) {
throw new BadRequestException("User with the provided e-mail already exists.");
}
@@ -73,7 +73,7 @@ export class OAuthClientUsersService {
};
}
async userExistsWithEmail(oAuthClientId: string, email: string) {
async managedUserExistsWithEmail(oAuthClientId: string, email: string) {
const oAuthEmail = this.getOAuthUserEmail(oAuthClientId, email);
const user = await this.userRepository.findByEmail(oAuthEmail);
return !!user;
+596 -125
View File
@@ -22,61 +22,7 @@
]
}
},
"/api/v2/events/public": {
"get": {
"operationId": "EventsController_getPublicEvent",
"parameters": [
{
"name": "username",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "eventSlug",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "isTeamEvent",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "org",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
},
"tags": [
"Event types"
]
}
},
"/api/v2/oauth-clients/{clientId}/users": {
"/v2/oauth-clients/{clientId}/users": {
"get": {
"operationId": "OAuthClientUsersController_getManagedUsers",
"parameters": [
@@ -144,7 +90,7 @@
]
}
},
"/api/v2/oauth-clients/{clientId}/users/{userId}": {
"/v2/oauth-clients/{clientId}/users/{userId}": {
"get": {
"operationId": "OAuthClientUsersController_getUserById",
"parameters": [
@@ -264,7 +210,7 @@
]
}
},
"/api/v2/oauth-clients/{clientId}/users/{userId}/force-refresh": {
"/v2/oauth-clients/{clientId}/users/{userId}/force-refresh": {
"post": {
"operationId": "OAuthClientUsersController_forceRefresh",
"parameters": [
@@ -302,7 +248,7 @@
]
}
},
"/api/v2/oauth-clients": {
"/v2/oauth-clients": {
"post": {
"operationId": "OAuthClientsController_createOAuthClient",
"summary": "",
@@ -356,7 +302,7 @@
]
}
},
"/api/v2/oauth-clients/{clientId}": {
"/v2/oauth-clients/{clientId}": {
"get": {
"operationId": "OAuthClientsController_getOAuthClientById",
"summary": "",
@@ -458,7 +404,7 @@
]
}
},
"/api/v2/oauth/{clientId}/authorize": {
"/v2/oauth/{clientId}/authorize": {
"post": {
"operationId": "OAuthFlowController_authorize",
"summary": "Authorize an OAuth client",
@@ -496,7 +442,7 @@
]
}
},
"/api/v2/oauth/{clientId}/exchange": {
"/v2/oauth/{clientId}/exchange": {
"post": {
"operationId": "OAuthFlowController_exchange",
"summary": "Exchange authorization code for access tokens",
@@ -549,7 +495,7 @@
]
}
},
"/api/v2/oauth/{clientId}/refresh": {
"/v2/oauth/{clientId}/refresh": {
"post": {
"operationId": "OAuthFlowController_refreshAccessToken",
"parameters": [
@@ -597,7 +543,7 @@
]
}
},
"/api/v2/event-types": {
"/v2/event-types": {
"post": {
"operationId": "EventTypesController_createEventType",
"parameters": [],
@@ -647,7 +593,7 @@
]
}
},
"/api/v2/event-types/{eventTypeId}": {
"/v2/event-types/{eventTypeId}": {
"get": {
"operationId": "EventTypesController_getEventType",
"parameters": [
@@ -743,7 +689,61 @@
]
}
},
"/api/v2/event-types/{username}/public": {
"/v2/event-types/{username}/{eventSlug}/public": {
"get": {
"operationId": "EventTypesController_getPublicEventType",
"parameters": [
{
"name": "username",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "eventSlug",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "isTeamEvent",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "org",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetEventTypePublicOutput"
}
}
}
}
},
"tags": [
"Event types"
]
}
},
"/v2/event-types/{username}/public": {
"get": {
"operationId": "EventTypesController_getPublicEventTypes",
"parameters": [
@@ -773,7 +773,7 @@
]
}
},
"/api/v2/ee/gcal/oauth/auth-url": {
"/v2/gcal/oauth/auth-url": {
"get": {
"operationId": "GcalController_redirect",
"parameters": [
@@ -803,7 +803,7 @@
]
}
},
"/api/v2/ee/gcal/oauth/save": {
"/v2/gcal/oauth/save": {
"get": {
"operationId": "GcalController_save",
"parameters": [
@@ -841,7 +841,7 @@
]
}
},
"/api/v2/ee/gcal/check": {
"/v2/gcal/check": {
"get": {
"operationId": "GcalController_check",
"parameters": [],
@@ -862,7 +862,7 @@
]
}
},
"/api/v2/ee/provider/{clientId}": {
"/v2/provider/{clientId}": {
"get": {
"operationId": "CalProviderController_verifyClientId",
"parameters": [
@@ -892,7 +892,7 @@
]
}
},
"/api/v2/ee/provider/{clientId}/access-token": {
"/v2/provider/{clientId}/access-token": {
"get": {
"operationId": "CalProviderController_verifyAccessToken",
"parameters": [
@@ -922,7 +922,7 @@
]
}
},
"/api/v2/schedules": {
"/v2/schedules": {
"post": {
"operationId": "SchedulesController_createSchedule",
"parameters": [],
@@ -972,7 +972,7 @@
]
}
},
"/api/v2/schedules/default": {
"/v2/schedules/default": {
"get": {
"operationId": "SchedulesController_getDefaultSchedule",
"parameters": [],
@@ -993,7 +993,7 @@
]
}
},
"/api/v2/schedules/{scheduleId}": {
"/v2/schedules/{scheduleId}": {
"get": {
"operationId": "SchedulesController_getSchedule",
"parameters": [
@@ -1089,7 +1089,7 @@
]
}
},
"/api/v2/ee/me": {
"/v2/me": {
"get": {
"operationId": "MeController_getMe",
"parameters": [],
@@ -1139,7 +1139,7 @@
]
}
},
"/api/v2/ee/calendars/busy-times": {
"/v2/calendars/busy-times": {
"get": {
"operationId": "CalendarsController_getBusyTimes",
"parameters": [],
@@ -1160,7 +1160,7 @@
]
}
},
"/api/v2/ee/calendars": {
"/v2/calendars": {
"get": {
"operationId": "CalendarsController_getCalendars",
"parameters": [],
@@ -1181,7 +1181,7 @@
]
}
},
"/api/v2/ee/bookings": {
"/v2/bookings": {
"get": {
"operationId": "BookingsController_getBookings",
"parameters": [
@@ -1235,7 +1235,16 @@
},
"post": {
"operationId": "BookingsController_createBooking",
"parameters": [],
"parameters": [
{
"name": "x-cal-client-id",
"required": true,
"in": "header",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
@@ -1263,7 +1272,7 @@
]
}
},
"/api/v2/ee/bookings/{bookingUid}": {
"/v2/bookings/{bookingUid}": {
"get": {
"operationId": "BookingsController_getBooking",
"parameters": [
@@ -1293,7 +1302,7 @@
]
}
},
"/api/v2/ee/bookings/{bookingUid}/reschedule": {
"/v2/bookings/{bookingUid}/reschedule": {
"get": {
"operationId": "BookingsController_getBookingForReschedule",
"parameters": [
@@ -1323,7 +1332,7 @@
]
}
},
"/api/v2/ee/bookings/{bookingId}/cancel": {
"/v2/bookings/{bookingId}/cancel": {
"post": {
"operationId": "BookingsController_cancelBooking",
"parameters": [
@@ -1334,6 +1343,14 @@
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"required": true,
"in": "header",
"schema": {
"type": "string"
}
}
],
"requestBody": {
@@ -1363,10 +1380,19 @@
]
}
},
"/api/v2/ee/bookings/reccuring": {
"/v2/bookings/recurring": {
"post": {
"operationId": "BookingsController_createReccuringBooking",
"parameters": [],
"operationId": "BookingsController_createRecurringBooking",
"parameters": [
{
"name": "x-cal-client-id",
"required": true,
"in": "header",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
@@ -1397,10 +1423,19 @@
]
}
},
"/api/v2/ee/bookings/instant": {
"/v2/bookings/instant": {
"post": {
"operationId": "BookingsController_createInstantBooking",
"parameters": [],
"parameters": [
{
"name": "x-cal-client-id",
"required": true,
"in": "header",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
@@ -1428,7 +1463,7 @@
]
}
},
"/api/v2/slots/reserve": {
"/v2/slots/reserve": {
"post": {
"operationId": "SlotsController_reserveSlot",
"parameters": [],
@@ -1459,7 +1494,7 @@
]
}
},
"/api/v2/slots/selected-slot": {
"/v2/slots/selected-slot": {
"delete": {
"operationId": "SlotsController_deleteSelectedSlot",
"parameters": [],
@@ -1480,7 +1515,7 @@
]
}
},
"/api/v2/slots/available": {
"/v2/slots/available": {
"get": {
"operationId": "SlotsController_getAvailableSlots",
"parameters": [],
@@ -1501,7 +1536,7 @@
]
}
},
"/api/v2/timezones": {
"/v2/timezones": {
"get": {
"operationId": "TimezonesController_getTimeZones",
"parameters": [],
@@ -1912,6 +1947,18 @@
"items": {
"type": "string"
}
},
"bookingRedirectUri": {
"type": "string"
},
"bookingCancelRedirectUri": {
"type": "string"
},
"bookingRescheduleRedirectUri": {
"type": "string"
},
"areEmailsEnabled": {
"type": "boolean"
}
}
},
@@ -2137,6 +2184,462 @@
"data"
]
},
"Location": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
},
"required": [
"type"
]
},
"Source": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string"
},
"label": {
"type": "string"
}
},
"required": [
"id",
"type",
"label"
]
},
"BookingField": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"type": {
"type": "string"
},
"defaultLabel": {
"type": "string"
},
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"required": {
"type": "boolean"
},
"getOptionsAt": {
"type": "string"
},
"hideWhenJustOneOption": {
"type": "boolean"
},
"editable": {
"type": "string"
},
"sources": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Source"
}
}
},
"required": [
"name",
"type"
]
},
"Organization": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"slug": {
"type": "string",
"nullable": true
},
"name": {
"type": "string"
},
"metadata": {
"type": "object"
}
},
"required": [
"id",
"name",
"metadata"
]
},
"Profile": {
"type": "object",
"properties": {
"username": {
"type": "string",
"nullable": true
},
"id": {
"type": "number",
"nullable": true
},
"userId": {
"type": "number"
},
"uid": {
"type": "string"
},
"name": {
"type": "string"
},
"organizationId": {
"type": "number",
"nullable": true
},
"organization": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/Organization"
}
]
},
"upId": {
"type": "string"
},
"image": {
"type": "string"
},
"brandColor": {
"type": "string"
},
"darkBrandColor": {
"type": "string"
},
"theme": {
"type": "string"
},
"bookerLayouts": {
"type": "object"
}
},
"required": [
"username",
"id",
"organizationId",
"upId"
]
},
"Owner": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"avatarUrl": {
"type": "string",
"nullable": true
},
"username": {
"type": "string",
"nullable": true
},
"name": {
"type": "string",
"nullable": true
},
"weekStart": {
"type": "string"
},
"brandColor": {
"type": "string",
"nullable": true
},
"darkBrandColor": {
"type": "string",
"nullable": true
},
"theme": {
"type": "string",
"nullable": true
},
"metadata": {
"type": "object"
},
"defaultScheduleId": {
"type": "number",
"nullable": true
},
"nonProfileUsername": {
"type": "string",
"nullable": true
},
"profile": {
"$ref": "#/components/schemas/Profile"
}
},
"required": [
"id",
"username",
"name",
"weekStart",
"metadata",
"nonProfileUsername",
"profile"
]
},
"Schedule": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"timeZone": {
"type": "string",
"nullable": true
}
},
"required": [
"id",
"timeZone"
]
},
"User": {
"type": "object",
"properties": {
"username": {
"type": "string",
"nullable": true
},
"name": {
"type": "string",
"nullable": true
},
"weekStart": {
"type": "string"
},
"organizationId": {
"type": "number"
},
"avatarUrl": {
"type": "string",
"nullable": true
},
"profile": {
"$ref": "#/components/schemas/Profile"
},
"bookerUrl": {
"type": "string"
}
},
"required": [
"username",
"name",
"weekStart",
"profile",
"bookerUrl"
]
},
"PublicEventTypeOutput": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"eventName": {
"type": "string",
"nullable": true
},
"slug": {
"type": "string"
},
"isInstantEvent": {
"type": "boolean"
},
"aiPhoneCallConfig": {
"type": "object"
},
"schedulingType": {
"type": "object"
},
"length": {
"type": "number"
},
"locations": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Location"
}
},
"customInputs": {
"type": "array",
"items": {
"type": "object"
}
},
"disableGuests": {
"type": "boolean"
},
"metadata": {
"type": "object",
"nullable": true
},
"lockTimeZoneToggleOnBookingPage": {
"type": "boolean"
},
"requiresConfirmation": {
"type": "boolean"
},
"requiresBookerEmailVerification": {
"type": "boolean"
},
"recurringEvent": {
"type": "object"
},
"price": {
"type": "number"
},
"currency": {
"type": "string"
},
"seatsPerTimeSlot": {
"type": "number",
"nullable": true
},
"seatsShowAvailabilityCount": {
"type": "boolean",
"nullable": true
},
"bookingFields": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BookingField"
}
},
"team": {
"type": "object"
},
"successRedirectUrl": {
"type": "string",
"nullable": true
},
"workflows": {
"type": "array",
"items": {
"type": "object"
}
},
"hosts": {
"type": "array",
"items": {
"type": "object"
}
},
"owner": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/Owner"
}
]
},
"schedule": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/Schedule"
}
]
},
"hidden": {
"type": "boolean"
},
"assignAllTeamMembers": {
"type": "boolean"
},
"bookerLayouts": {
"type": "object"
},
"users": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
},
"entity": {
"type": "object"
},
"isDynamic": {
"type": "boolean"
}
},
"required": [
"id",
"title",
"description",
"slug",
"isInstantEvent",
"length",
"locations",
"customInputs",
"disableGuests",
"metadata",
"lockTimeZoneToggleOnBookingPage",
"requiresConfirmation",
"requiresBookerEmailVerification",
"price",
"currency",
"seatsShowAvailabilityCount",
"bookingFields",
"workflows",
"hosts",
"owner",
"schedule",
"hidden",
"assignAllTeamMembers",
"users",
"entity",
"isDynamic"
]
},
"GetEventTypePublicOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/PublicEventTypeOutput"
}
]
}
},
"required": [
"status",
"data"
]
},
"PublicEventType": {
"type": "object",
"properties": {
@@ -3357,26 +3860,6 @@
"credentialId"
]
},
"User": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string",
"nullable": true
},
"email": {
"type": "string"
}
},
"required": [
"id",
"name",
"email"
]
},
"GetBookingsDataEntry": {
"type": "object",
"properties": {
@@ -3663,21 +4146,6 @@
"data"
]
},
"Location": {
"type": "object",
"properties": {
"optionValue": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"optionValue",
"value"
]
},
"Response": {
"type": "object",
"properties": {
@@ -3757,6 +4225,9 @@
},
"responses": {
"$ref": "#/components/schemas/Response"
},
"locationUrl": {
"type": "string"
}
},
"required": [
-1
View File
@@ -1 +0,0 @@
Hello World
-1
View File
@@ -1 +0,0 @@
## Cal.com Platform Example (https://platform.cal.com)
+9
View File
@@ -1,5 +1,14 @@
# @calcom/web
## 3.9.9
### Patch Changes
- Updated dependencies
- @calcom/embed-core@1.4.0
- @calcom/embed-react@1.4.0
- @calcom/embed-snippet@1.2.0
## 3.1.3
### Patch Changes
+1
View File
@@ -0,0 +1 @@
export { GET } from "@calcom/features/tasker/api/cleanup";
+1
View File
@@ -0,0 +1 @@
export { GET } from "@calcom/features/tasker/api/cron";
+3 -3
View File
@@ -33,7 +33,7 @@ interface AppListProps {
export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppListProps) => {
const { data: defaultConferencingApp } = trpc.viewer.getUsersDefaultConferencingApp.useQuery();
const utils = trpc.useContext();
const utils = trpc.useUtils();
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>(
undefined
@@ -139,7 +139,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
...app,
credentialOwner: {
name: team.name,
avatar: team.logo,
avatar: team.logoUrl,
teamId: team.teamId,
credentialId: team.credentialId,
readOnly: !team.isAdmin,
@@ -201,7 +201,7 @@ function ConnectOrDisconnectIntegrationMenuItem(props: {
const { type, credentialId, isGlobal, installed, handleDisconnect, teamId } = props;
const { t } = useLocale();
const utils = trpc.useContext();
const utils = trpc.useUtils();
const handleOpenChange = () => {
utils.viewer.integrations.invalidate();
};
@@ -192,7 +192,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const utils = trpc.useContext();
const utils = trpc.useUtils();
const onChanged = () =>
Promise.allSettled([
utils.viewer.integrations.invalidate(
@@ -2,7 +2,7 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { doesAppSupportTeamInstall } from "@calcom/app-store/utils";
import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner";
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import type { AppFrontendPayload } from "@calcom/types/App";
@@ -124,8 +124,8 @@ export const InstallAppButtonChild = ({
disabled={isInstalled}
CustomStartIcon={
<Avatar
alt={team.logo || ""}
imageSrc={team.logo || `${WEBAPP_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
alt={team.logoUrl || ""}
imageSrc={getPlaceholderAvatar(team.logoUrl, team.name)} // if no image, use default avatar
size="sm"
/>
}
@@ -64,7 +64,7 @@ function BookingListItem(booking: BookingItemProps) {
t,
i18n: { language },
} = useLocale();
const utils = trpc.useContext();
const utils = trpc.useUtils();
const [rejectionReason, setRejectionReason] = useState<string>("");
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
@@ -24,7 +24,7 @@ interface IRescheduleDialog {
export const ChargeCardDialog = (props: IRescheduleDialog) => {
const { t } = useLocale();
const utils = trpc.useContext();
const utils = trpc.useUtils();
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
const [chargeError, setChargeError] = useState(false);
const chargeCardMutation = trpc.viewer.payments.chargeCard.useMutation({
@@ -23,7 +23,7 @@ interface IRescheduleDialog {
export const RescheduleDialog = (props: IRescheduleDialog) => {
const { t } = useLocale();
const utils = trpc.useContext();
const utils = trpc.useUtils();
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
const [rescheduleReason, setRescheduleReason] = useState("");
@@ -115,7 +115,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
// credentialIds: team?.credentialId ? [team.credentialId] : [],
credentialOwner: {
name: team.name,
avatar: team.logo,
avatar: team.logoUrl,
teamId: team.teamId,
credentialId: team.credentialId,
},
@@ -480,6 +480,7 @@ export const EventSetupTab = (
label={t("title")}
{...(isManagedEventType || isChildrenManagedEventType ? titleLockedProps : {})}
defaultValue={eventType.title}
data-testid="event-title"
{...formMethods.register("title")}
/>
<div>
@@ -494,6 +495,7 @@ export const EventSetupTab = (
label={t("URL")}
{...(isManagedEventType || isChildrenManagedEventType ? urlLockedProps : {})}
defaultValue={eventType.slug}
data-testid="event-slug"
addOnLeading={
<>
{urlPrefix}/
@@ -123,7 +123,7 @@ function DeleteDialog({
open,
onOpenChange,
}: { isManagedEvent: string; eventTypeId: number } & Pick<DialogProps, "open" | "onOpenChange">) {
const utils = trpc.useContext();
const utils = trpc.useUtils();
const { t } = useLocale();
const router = useRouter();
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
@@ -481,7 +481,6 @@ function EventTypeSingleLayout({
sticky
linkShallow
itemClassname="items-start"
iconClassName="md:mt-px"
/>
</div>
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
@@ -19,7 +19,7 @@ import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@c
export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
const { t } = useLocale();
const utils = trpc.useContext();
const utils = trpc.useUtils();
const formMethods = useFormContext<FormValues>();
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({ eventTypeId: eventType.id });
@@ -99,7 +99,7 @@ export default function InstantEventController({
const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();
const utils = trpc.useContext();
const utils = trpc.useUtils();
const formMethods = useFormContext<FormValues>();
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({
@@ -25,7 +25,7 @@ const UserProfile = () => {
const { data: eventTypes } = trpc.viewer.eventTypes.list.useQuery();
const [imageSrc, setImageSrc] = useState<string>(user?.avatar || "");
const utils = trpc.useContext();
const utils = trpc.useUtils();
const router = useRouter();
const createEventType = trpc.viewer.eventTypes.create.useMutation();
const telemetry = useTelemetry();
@@ -33,7 +33,7 @@ const UserProfile = () => {
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async (_data, context) => {
if (context.avatar) {
if (context.avatarUrl) {
showToast(t("your_user_profile_updated_successfully"), "success");
await utils.viewer.me.refetch();
} else
@@ -74,7 +74,7 @@ const UserProfile = () => {
event.preventDefault();
const enteredAvatar = avatarRef.current?.value;
mutation.mutate({
avatar: enteredAvatar,
avatarUrl: enteredAvatar,
});
}
@@ -48,7 +48,7 @@ const UserSettings = (props: IUserSettingsProps) => {
telemetry.event(telemetryEventTypes.onboardingStarted);
}, [telemetry]);
const utils = trpc.useContext();
const utils = trpc.useUtils();
const onSuccess = async () => {
await utils.viewer.me.invalidate();
nextStep();
@@ -3,7 +3,7 @@ import { trpc } from "@calcom/trpc/react";
import { Badge, Button, showToast } from "@calcom/ui";
const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonation: boolean }) => {
const utils = trpc.useContext();
const utils = trpc.useUtils();
const { t } = useLocale();
@@ -0,0 +1,160 @@
import type { FormValues } from "@pages/settings/my-account/general";
import { useState } from "react";
import type { UseFormSetValue } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import {
Dialog,
DialogContent,
DialogFooter,
DialogClose,
Button,
Label,
DateRangePicker,
TimezoneSelect,
SettingsToggle,
DatePicker,
} from "@calcom/ui";
interface TravelScheduleModalProps {
open: boolean;
onOpenChange: () => void;
setValue: UseFormSetValue<FormValues>;
existingSchedules: FormValues["travelSchedules"];
}
const TravelScheduleModal = ({
open,
onOpenChange,
setValue,
existingSchedules,
}: TravelScheduleModalProps) => {
const { t } = useLocale();
const { timezone: preferredTimezone } = useTimePreferences();
const [startDate, setStartDate] = useState<Date>(new Date());
const [endDate, setEndDate] = useState<Date | undefined>(new Date());
const [selectedTimeZone, setSelectedTimeZone] = useState(preferredTimezone);
const [isNoEndDate, setIsNoEndDate] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const isOverlapping = (newSchedule: { startDate: Date; endDate?: Date }) => {
const newStart = dayjs(newSchedule.startDate);
const newEnd = newSchedule.endDate ? dayjs(newSchedule.endDate) : null;
for (const schedule of existingSchedules) {
const start = dayjs(schedule.startDate);
const end = schedule.endDate ? dayjs(schedule.endDate) : null;
if (!newEnd) {
// if the start date is after or on the existing schedule's start date and before the existing schedule's end date (if it has one)
if (newStart.isSame(start) || newStart.isAfter(start)) {
if (!end || newStart.isSame(end) || newStart.isBefore(end)) return true;
}
} else {
// For schedules with an end date, check for any overlap
if (newStart.isSame(end) || newStart.isBefore(end) || end === null) {
if (newEnd.isSame(start) || newEnd.isAfter(start)) {
return true;
}
}
}
}
};
const resetValues = () => {
setStartDate(new Date());
setEndDate(new Date());
setSelectedTimeZone(preferredTimezone);
setIsNoEndDate(false);
};
const createNewSchedule = () => {
const newSchedule = {
startDate,
endDate,
timeZone: selectedTimeZone,
};
if (!isOverlapping(newSchedule)) {
setValue("travelSchedules", existingSchedules.concat(newSchedule), { shouldDirty: true });
onOpenChange();
resetValues();
} else {
setErrorMessage(t("overlaps_with_existing_schedule"));
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
title={t("travel_schedule")}
description={t("travel_schedule_description")}
type="creation">
<div>
{!isNoEndDate ? (
<>
<Label className="mt-2">{t("time_range")}</Label>
<DateRangePicker
startDate={startDate}
endDate={endDate ?? startDate}
onDatesChange={({ startDate: newStartDate, endDate: newEndDate }) => {
setStartDate(newStartDate);
setEndDate(newEndDate);
setErrorMessage("");
}}
/>
</>
) : (
<>
<Label className="mt-2">{t("date")}</Label>
<DatePicker
minDate={new Date()}
date={startDate}
className="w-56"
onDatesChange={(newDate) => {
setStartDate(newDate);
setErrorMessage("");
}}
/>
</>
)}
<div className="text-error mt-1 text-sm">{errorMessage}</div>
<div className="mt-3">
<SettingsToggle
labelClassName="mt-1 font-normal"
title={t("schedule_tz_without_end_date")}
checked={isNoEndDate}
onCheckedChange={(e) => {
setEndDate(!e ? startDate : undefined);
setIsNoEndDate(e);
setErrorMessage("");
}}
/>
</div>
<Label className="mt-6">{t("timezone")}</Label>
<TimezoneSelect
id="timeZone"
value={selectedTimeZone}
onChange={({ value }) => setSelectedTimeZone(value)}
className="mb-11 mt-2 w-full rounded-md text-sm"
/>
</div>
<DialogFooter showDivider className="relative">
<DialogClose />
<Button
onClick={() => {
createNewSchedule();
}}>
{t("add")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default TravelScheduleModal;
@@ -1,3 +1,4 @@
import { useRouter } from "next/navigation";
import React from "react";
import { classNames } from "@calcom/lib";
@@ -38,7 +39,7 @@ export const OAuthClientCard = ({
isLoading,
areEmailsEnabled,
}: OAuthClientCardProps) => {
console.log(areEmailsEnabled);
const router = useRouter();
const clientPermissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value, index) => {
let permissionsMessage = "";
@@ -139,7 +140,14 @@ export const OAuthClientCard = ({
<span className="text-sm font-semibold">Emails enabled:</span> {areEmailsEnabled ? "Yes" : "No"}
</div>
</div>
<div className="flex items-start">
<div className="flex items-start gap-4">
<Button
className="bg-subtle hover:bg-emphasis text-white"
loading={isLoading}
disabled={isLoading}
onClick={() => router.push(`/settings/organizations/platform/oauth-clients/create?clientId=${id}`)}>
Edit
</Button>
<Button
className="bg-red-500 text-white hover:bg-red-600"
loading={isLoading}
@@ -1,6 +1,6 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import type { FC } from "react";
import React, { useState, useCallback } from "react";
import React, { useState, useCallback, useEffect } from "react";
import { useForm, useFieldArray } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -8,7 +8,24 @@ import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions"
import { showToast } from "@calcom/ui";
import { Meta, Button, TextField, Label, Tooltip } from "@calcom/ui";
import { useCreateOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient";
import { useOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/useOAuthClients";
import {
useCreateOAuthClient,
useUpdateOAuthClient,
} from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient";
import {
hasAppsReadPermission,
hasAppsWritePermission,
hasBookingReadPermission,
hasBookingWritePermission,
hasEventTypeReadPermission,
hasEventTypeWritePermission,
hasProfileReadPermission,
hasProfileWritePermission,
hasScheduleReadPermission,
hasScheduleWritePermission,
} from "../../../../../../../packages/platform/utils/permissions";
type FormValues = {
name: string;
@@ -33,9 +50,10 @@ type FormValues = {
areEmailsEnabled?: boolean;
};
export const OAuthClientForm: FC = () => {
export const OAuthClientForm: FC<{ clientId?: string }> = ({ clientId }) => {
const { t } = useLocale();
const router = useRouter();
const { data, isFetched, isError, refetch } = useOAuthClient(clientId);
const { register, control, handleSubmit, setValue } = useForm<FormValues>({
defaultValues: {
redirectUris: [{ uri: "" }],
@@ -45,6 +63,32 @@ export const OAuthClientForm: FC = () => {
control,
name: "redirectUris",
});
useEffect(() => {
if (isFetched && data && !isError) {
setValue("name", data.name);
data.bookingRedirectUri && setValue("bookingRedirectUri", data.bookingRedirectUri);
data.bookingCancelRedirectUri && setValue("bookingCancelRedirectUri", data.bookingCancelRedirectUri);
data.bookingRescheduleRedirectUri &&
setValue("bookingRescheduleRedirectUri", data.bookingRescheduleRedirectUri);
setValue("areEmailsEnabled", data?.areEmailsEnabled);
data?.redirectUris.forEach((uri: string, index: number) => {
index === 0 && setValue(`redirectUris.${index}.uri`, uri);
index !== 0 && append({ uri });
});
if (hasAppsReadPermission(data.permissions)) setValue("appsRead", true);
if (hasAppsWritePermission(data.permissions)) setValue("appsWrite", true);
if (hasBookingReadPermission(data.permissions)) setValue("bookingRead", true);
if (hasBookingWritePermission(data.permissions)) setValue("bookingWrite", true);
if (hasEventTypeReadPermission(data.permissions)) setValue("eventTypeRead", true);
if (hasEventTypeWritePermission(data.permissions)) setValue("eventTypeWrite", true);
if (hasProfileReadPermission(data.permissions)) setValue("profileRead", true);
if (hasProfileWritePermission(data.permissions)) setValue("profileWrite", true);
if (hasScheduleReadPermission(data.permissions)) setValue("scheduleRead", true);
if (hasScheduleWritePermission(data.permissions)) setValue("scheduleWrite", true);
}
}, [isFetched, data]);
const disabledForm = Boolean(clientId && !isFetched && isError);
const [isSelectAllPermissionsChecked, setIsSelectAllPermissionsChecked] = useState(false);
const selectAllPermissions = useCallback(() => {
@@ -59,15 +103,27 @@ export const OAuthClientForm: FC = () => {
setIsSelectAllPermissionsChecked((preValue) => !preValue);
}, [isSelectAllPermissionsChecked, setValue]);
const { mutateAsync, isPending } = useCreateOAuthClient({
const { mutateAsync: save, isPending: isSaving } = useCreateOAuthClient({
onSuccess: () => {
showToast("OAuth client created successfully", "success");
refetch();
router.push("/settings/organizations/platform/oauth-clients");
},
onError: () => {
showToast("Internal server error, please try again later", "error");
},
});
const { mutateAsync: update, isPending: isUpdating } = useUpdateOAuthClient({
onSuccess: () => {
showToast("OAuth client updated successfully", "success");
refetch();
router.push("/settings/organizations/platform/oauth-clients");
},
onError: () => {
showToast("Internal server error, please try again later", "error");
},
clientId,
});
const onSubmit = (data: FormValues) => {
let userPermissions = 0;
@@ -82,17 +138,31 @@ export const OAuthClientForm: FC = () => {
if (data[`${entityKey}Write`]) userPermissions |= write;
});
mutateAsync({
name: data.name,
permissions: userPermissions,
// logo: data.logo,
redirectUris: userRedirectUris,
bookingRedirectUri: data.bookingRedirectUri,
bookingCancelRedirectUri: data.bookingCancelRedirectUri,
bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri,
areEmailsEnabled: data.areEmailsEnabled,
});
if (clientId) {
// don't update permissions if client is already created
update({
name: data.name,
// logo: data.logo,
redirectUris: userRedirectUris,
bookingRedirectUri: data.bookingRedirectUri,
bookingCancelRedirectUri: data.bookingCancelRedirectUri,
bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri,
areEmailsEnabled: data.areEmailsEnabled,
});
} else {
save({
name: data.name,
permissions: userPermissions,
// logo: data.logo,
redirectUris: userRedirectUris,
bookingRedirectUri: data.bookingRedirectUri,
bookingCancelRedirectUri: data.bookingCancelRedirectUri,
bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri,
areEmailsEnabled: data.areEmailsEnabled,
});
}
};
const isPending = isSaving || isUpdating;
const permissionsCheckboxes = Object.keys(PERMISSIONS_GROUPED_MAP).map((key) => {
const entity = key as keyof typeof PERMISSIONS_GROUPED_MAP;
@@ -109,6 +179,7 @@ export const OAuthClientForm: FC = () => {
id={`${permissionKey}Read`}
className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed"
type="checkbox"
disabled={Boolean(clientId)}
/>
<label htmlFor={`${permissionKey}Read`} className="cursor-pointer text-sm">
Read
@@ -120,6 +191,7 @@ export const OAuthClientForm: FC = () => {
id={`${permissionKey}Write`}
className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed"
type="checkbox"
disabled={Boolean(clientId)}
/>
<label htmlFor={`${permissionKey}Write`} className="cursor-pointer text-sm">
Write
@@ -133,15 +205,15 @@ export const OAuthClientForm: FC = () => {
return (
<div>
<Meta
title={t("oauth_form_title")}
description={t("oauth_form_description")}
title={t("oauth_form_title") + (clientId ? " - Update" : "")}
description={t("oauth_form_description") + (clientId ? " - Update" : "")}
borderInShellHeader={true}
/>
<form
className="border-subtle rounded-b-lg border border-t-0 px-4 pb-8 pt-2"
onSubmit={handleSubmit(onSubmit)}>
<div className="mt-6">
<TextField required={true} label="Client name" {...register("name")} />
<TextField disabled={disabledForm} required={true} label="Client name" {...register("name")} />
</div>
<div className="mt-6">
<Label>Redirect uris</Label>
@@ -154,6 +226,7 @@ export const OAuthClientForm: FC = () => {
required={index === 0}
className="w-[100%]"
label=""
disabled={disabledForm}
{...register(`redirectUris.${index}.uri` as const)}
/>
</div>
@@ -168,6 +241,7 @@ export const OAuthClientForm: FC = () => {
onClick={() => {
append({ uri: "" });
}}
disabled={disabledForm}
/>
{index > 0 && (
<Button
@@ -180,6 +254,7 @@ export const OAuthClientForm: FC = () => {
onClick={() => {
remove(index);
}}
disabled={disabledForm}
/>
)}
</div>
@@ -224,6 +299,7 @@ export const OAuthClientForm: FC = () => {
label="Booking redirect uri"
className="w-[100%]"
{...register("bookingRedirectUri")}
disabled={disabledForm}
/>
</Tooltip>
</div>
@@ -234,6 +310,7 @@ export const OAuthClientForm: FC = () => {
label="Booking cancel redirect uri"
className="w-[100%]"
{...register("bookingCancelRedirectUri")}
disabled={disabledForm}
/>
</Tooltip>
</div>
@@ -244,6 +321,7 @@ export const OAuthClientForm: FC = () => {
label="Booking reschedule redirect uri"
className="w-[100%]"
{...register("bookingRescheduleRedirectUri")}
disabled={disabledForm}
/>
</Tooltip>
</div>
@@ -253,22 +331,27 @@ export const OAuthClientForm: FC = () => {
id="areEmailsEnabled"
className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed"
type="checkbox"
disabled={disabledForm}
/>
<label htmlFor="areEmailsEnabled" className="cursor-pointer px-2 text-base font-semibold">
Enable emails
</label>
</div>
<div className="mt-6">
<div className="flex justify-between">
<h1 className="text-base font-semibold underline">Permissions</h1>
<Button type="button" onClick={selectAllPermissions}>
<Tooltip side="right" content="Permissions once set cannot be modified">
<h1 className="text-base font-semibold underline">Permissions</h1>
</Tooltip>
<Button type="button" onClick={selectAllPermissions} disabled={disabledForm || Boolean(clientId)}>
{!isSelectAllPermissionsChecked ? "Select all" : "Discard all"}
</Button>
</div>
<div>{permissionsCheckboxes}</div>
</div>
<Button className="mt-6" type="submit" loading={isPending}>
Submit
<Button className="mt-6" type="submit" loading={isPending} disabled={disabledForm}>
{clientId ? "Update" : "Submit"}
</Button>
</form>
</div>
+1
View File
@@ -72,6 +72,7 @@ export const getEventTypesFromDB = async (id: number) => {
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
schedulingType: true,
periodStartDate: true,
periodEndDate: true,
},
@@ -52,7 +52,6 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
});
let name: string;
let isAway = false;
let hideBranding = false;
const notFound = {
@@ -96,7 +95,6 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
}
name = username;
isAway = user.away;
hideBranding = user.hideBranding;
}
@@ -129,7 +127,6 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
eventData.length
),
booking,
away: isAway,
user: name,
slug,
trpcState: ssr.dehydrate(),
@@ -33,7 +33,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
select: {
id: true,
name: true,
logo: true,
logoUrl: true,
},
},
},
@@ -17,20 +17,38 @@ export const useOAuthClients = () => {
return { ...query, data: query.data?.data ?? [] };
};
export const useOAuthClient = (clientId: string) => {
export const useOAuthClient = (clientId?: string) => {
const {
isLoading,
error,
data: response,
isFetched,
isError,
isFetching,
isSuccess,
isFetchedAfterMount,
refetch,
} = useQuery<ApiSuccessResponse<PlatformOAuthClient>>({
queryKey: ["oauth-client"],
queryKey: ["oauth-client", clientId],
queryFn: () => {
return fetch(`/api/v2/oauth-clients/${clientId}`, {
method: "get",
headers: { "Content-type": "application/json" },
}).then((res) => res.json());
},
enabled: clientId !== undefined,
staleTime: Infinity,
});
return { isLoading, error, data: response?.data };
return {
isLoading,
error,
data: response?.data,
isFetched,
isError,
isFetching,
isSuccess,
isFetchedAfterMount,
refetch,
};
};
@@ -46,6 +46,43 @@ export const useCreateOAuthClient = (
return mutation;
};
export const useUpdateOAuthClient = (
{ onSuccess, onError, clientId }: IPersistOAuthClient & { clientId?: string } = {
onSuccess: () => {
return;
},
onError: () => {
return;
},
}
) => {
const mutation = useMutation<
ApiResponse<{ clientId: string; clientSecret: string }>,
unknown,
Omit<CreateOAuthClientInput, "permissions">
>({
mutationFn: (data) => {
return fetch(`/api/v2/oauth-clients/${clientId}`, {
method: "PATCH",
headers: { "Content-type": "application/json" },
body: JSON.stringify(data),
}).then((res) => res?.json());
},
onSuccess: (data) => {
if (data.status === SUCCESS_STATUS) {
onSuccess?.();
} else {
onError?.();
}
},
onError: () => {
onError?.();
},
});
return mutation;
};
export const useDeleteOAuthClient = (
{ onSuccess, onError }: IPersistOAuthClient = {
onSuccess: () => {
@@ -62,7 +99,7 @@ export const useDeleteOAuthClient = (
return fetch(`/api/v2/oauth-clients/${id}`, {
method: "delete",
headers: { "Content-type": "application/json" },
}).then((res) => res.json());
}).then((res) => res?.json());
},
onSuccess: (data) => {
if (data.status === SUCCESS_STATUS) {
+8 -2
View File
@@ -53,7 +53,10 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || signupDisabled) {
return {
notFound: true,
redirect: {
permanent: false,
destination: `/auth/error?error=Signup is disabled in this instance`,
},
} as const;
}
@@ -98,7 +101,10 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
if (!verificationToken || verificationToken.expires < new Date()) {
return {
notFound: true,
redirect: {
permanent: false,
destination: `/auth/error?error=Verification Token is missing or has expired`,
},
} as const;
}
@@ -89,7 +89,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
metadata: eventData.metadata,
},
booking,
away: false,
user: teamSlug,
teamId: team.id,
slug: meetingSlug,
@@ -98,6 +98,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
isPrivate: true,
isOrganization: true,
metadata: true,
logoUrl: true,
},
},
},
@@ -213,6 +214,9 @@ function getTeamWithoutMetadata<T extends Pick<Team, "metadata">>(team: T) {
const teamMetadata = teamMetadataSchema.parse(metadata);
return {
...rest,
requestedSlug: teamMetadata?.requestedSlug,
// add requestedSlug if available.
...(typeof teamMetadata?.requestedSlug !== "undefined"
? { requestedSlug: teamMetadata?.requestedSlug }
: {}),
};
}
+9
View File
@@ -16,3 +16,12 @@ export type WorkingHours = {
startTime: number;
endTime: number;
};
export type TravelSchedule = {
id: number;
timeZone: string;
userId: number;
startDate: Date;
endDate: Date | null;
prevTimeZone: string | null;
};
@@ -44,11 +44,12 @@ import useTheme from "@calcom/lib/hooks/useTheme";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
import { localStorage } from "@calcom/lib/webstorage";
import { BookingStatus } from "@calcom/prisma/enums";
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import {
Alert,
Avatar,
Badge,
Button,
EmailInput,
@@ -225,6 +226,7 @@ export default function Success(props: PageProps) {
const giphyAppData = getEventTypeAppData(eventType, "giphy");
const giphyImage = giphyAppData?.thankYouPage;
const isRoundRobin = eventType.schedulingType === SchedulingType.ROUND_ROBIN;
const eventName = getEventName(eventNameObject, true);
// Confirmation can be needed in two cases as of now
@@ -294,6 +296,7 @@ export default function Success(props: PageProps) {
function getTitle(): string {
const titleSuffix = props.recurringBookings ? "_recurring" : "";
const titlePrefix = isRoundRobin ? "round_robin_" : "";
if (isCancelled) {
return "";
}
@@ -305,6 +308,11 @@ export default function Success(props: PageProps) {
}
return t(`needs_to_be_confirmed_or_rejected${titleSuffix}`);
}
if (bookingInfo.user) {
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
user: bookingInfo.user.name || bookingInfo.user.email,
});
}
return t(`emailed_you_and_attendees${titleSuffix}`);
}
@@ -396,27 +404,36 @@ export default function Success(props: PageProps) {
{!isFeedbackMode && (
<>
<div
className={classNames(
"mx-auto flex items-center justify-center",
!giphyImage && !isCancelled && !needsConfirmation
? "bg-success h-12 w-12 rounded-full"
: "",
!giphyImage && !isCancelled && needsConfirmation
? "bg-subtle h-12 w-12 rounded-full"
: "",
isCancelled ? "bg-error h-12 w-12 rounded-full " : ""
)}>
{giphyImage && !needsConfirmation && !isCancelled && (
// eslint-disable-next-line @next/next/no-img-element
<img src={giphyImage} alt="Gif from Giphy" />
className={classNames(isRoundRobin && "min-w-32 min-h-24 relative mx-auto h-24 w-32")}>
{isRoundRobin && bookingInfo.user && (
<Avatar
className="mx-auto flex items-center justify-center"
alt={bookingInfo.user.name || bookingInfo.user.email}
size="xl"
imageSrc={`${bookingInfo.user.avatarUrl}`}
/>
)}
{!giphyImage && !needsConfirmation && !isCancelled && (
<Icon name="check" className="h-5 w-5 text-green-600 dark:text-green-400" />
)}
{needsConfirmation && !isCancelled && (
<Icon name="calendar" className="text-emphasis h-5 w-5" />
)}
{isCancelled && <Icon name="x" className="h-5 w-5 text-red-600 dark:text-red-200" />}
<div
className={classNames(
"mx-auto flex h-12 w-12 items-center justify-center rounded-full",
isRoundRobin &&
"border-cal-bg dark:border-cal-bg-muted absolute bottom-0 right-0 z-10 h-12 w-12 border-8",
!giphyImage && !isCancelled && !needsConfirmation ? "bg-success" : "",
!giphyImage && !isCancelled && needsConfirmation ? "bg-subtle" : "",
isCancelled ? "bg-error" : ""
)}>
{giphyImage && !needsConfirmation && !isCancelled && (
// eslint-disable-next-line @next/next/no-img-element
<img src={giphyImage} alt="Gif from Giphy" />
)}
{!giphyImage && !needsConfirmation && !isCancelled && (
<Icon name="check" className="h-5 w-5 text-green-600 dark:text-green-400" />
)}
{needsConfirmation && !isCancelled && (
<Icon name="calendar" className="text-emphasis h-5 w-5" />
)}
{isCancelled && <Icon name="x" className="h-5 w-5 text-red-600 dark:text-red-200" />}
</div>
</div>
<div className="mb-8 mt-6 text-center last:mb-0">
<h3
@@ -463,7 +480,7 @@ export default function Success(props: PageProps) {
)}
<div className="font-medium">{t("what")}</div>
<div className="col-span-2 mb-6 last:mb-0" data-testid="booking-title">
{eventName}
{isRoundRobin ? bookingInfo.title : eventName}
</div>
<div className="font-medium">{t("when")}</div>
<div className="col-span-2 mb-6 last:mb-0">
@@ -860,10 +877,10 @@ export default function Success(props: PageProps) {
</>
) : (
<>
<div className="my-3 flex justify-end">
<div className="my-3 flex justify-center space-x-1">
<button
className={classNames(
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
rateValue === 1
? "border-focus bg-emphasis"
: "border-muted bg-default opacity-50"
@@ -874,7 +891,7 @@ export default function Success(props: PageProps) {
</button>
<button
className={classNames(
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
rateValue === 2
? "border-focus bg-emphasis"
: "border-muted bg-default opacity-50"
@@ -885,7 +902,7 @@ export default function Success(props: PageProps) {
</button>
<button
className={classNames(
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
rateValue === 3
? "border-focus bg-emphasis"
: " border-muted bg-default opacity-50"
@@ -896,7 +913,7 @@ export default function Success(props: PageProps) {
</button>
<button
className={classNames(
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
rateValue === 4
? "border-focus bg-emphasis"
: "border-muted bg-default opacity-50"
@@ -907,7 +924,7 @@ export default function Success(props: PageProps) {
</button>
<button
className={classNames(
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
rateValue === 5
? "border-focus bg-emphasis"
: "border-muted bg-default opacity-50"
@@ -230,7 +230,7 @@ export const EventTypeList = ({
const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = useState<SchedulingType | null>(
null
);
const utils = trpc.useContext();
const utils = trpc.useUtils();
const mutation = trpc.viewer.eventTypeOrder.useMutation({
onError: async (err) => {
console.error(err.message);
@@ -896,7 +896,7 @@ const Main = ({
className="mt-4 flex flex-col"
data-testid={`slug-${group.profile.slug}`}
key={group.profile.slug}>
{/* If the group is readonly and empty don't leave a floating header when the user cant see the create box due
{/* If the group is readonly and empty don't leave a floating header when the user cant see the create box due
to it being readonly for that user */}
{group.eventTypes.length === 0 && group.metadata.readOnly ? null : (
<EventTypeListHeading
@@ -34,11 +34,26 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
} as const;
return redirect;
}
await ssr.viewer.eventTypes.get.prefetch({ id: typeParam });
const { eventType } = await ssr.viewer.eventTypes.get.fetch({ id: typeParam });
const getEventTypeById = async (eventTypeId: number) => {
await ssr.viewer.eventTypes.get.prefetch({ id: eventTypeId });
try {
const { eventType } = await ssr.viewer.eventTypes.get.fetch({ id: eventTypeId });
return eventType;
} catch (e: unknown) {
// reject, user has no access to this event type.
return null;
}
};
const eventType = await getEventTypeById(typeParam);
if (!eventType) {
const redirect = {
redirect: {
permanent: false,
destination: "/event-types",
},
} as const;
return redirect;
}
return {
props: {
eventType,
@@ -141,7 +141,7 @@ export type EventTypeSetup = RouterOutputs["viewer"]["eventTypes"]["get"]["event
const EventTypePage = (props: EventTypeSetupProps) => {
const { t } = useLocale();
const utils = trpc.useContext();
const utils = trpc.useUtils();
const telemetry = useTelemetry();
const {
data: { tabName },
@@ -767,7 +767,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
}, {});
if (dirtyFieldExists) {
updateMutation.mutate({ ...filteredPayload, id: eventType.id });
updateMutation.mutate({ ...filteredPayload, id: eventType.id, hashedLink: values.hashedLink });
}
}}>
<div ref={animationParentRef}>{tabMap[tabName]}</div>
@@ -39,13 +39,14 @@ export type UserPageProps = {
allowSEOIndexing: boolean;
username: string | null;
};
users: (Pick<User, "away" | "name" | "username" | "bio" | "verified" | "avatarUrl"> & {
users: (Pick<User, "name" | "username" | "bio" | "verified" | "avatarUrl"> & {
profile: UserProfile;
})[];
themeBasis: string | null;
markdownStrippedBio: string;
safeBio: string;
entity: {
logoUrl?: string | null;
considerUnpublished: boolean;
orgSlug?: string | null;
name?: string | null;
@@ -98,12 +99,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
});
const usersWithoutAvatar = usersInOrgContext.map((user) => {
const { avatar: _1, ...rest } = user;
return rest;
});
const isDynamicGroup = usersWithoutAvatar.length > 1;
const isDynamicGroup = usersInOrgContext.length > 1;
log.debug(safeStringify({ usersInOrgContext, isValidOrgDomain, currentOrgDomain, isDynamicGroup }));
if (isDynamicGroup) {
@@ -114,40 +110,27 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
permanent: false,
destination: destinationUrl,
},
} as {
redirect: {
permanent: false;
destination: string;
};
};
} as const;
}
const users = usersWithoutAvatar.map((user) => ({
...user,
avatar: `/${user.username}/avatar.png`,
}));
const isNonOrgUser = (user: { profile: UserProfile }) => {
return !user.profile?.organization;
};
const isThereAnyNonOrgUser = users.some(isNonOrgUser);
const isThereAnyNonOrgUser = usersInOrgContext.some(isNonOrgUser);
if (!users.length || (!isValidOrgDomain && !isThereAnyNonOrgUser)) {
if (!usersInOrgContext.length || (!isValidOrgDomain && !isThereAnyNonOrgUser)) {
return {
notFound: true,
} as {
notFound: true;
};
} as const;
}
const [user] = users; //to be used when dealing with single user, not dynamic group
const [user] = usersInOrgContext; //to be used when dealing with single user, not dynamic group
const profile = {
name: user.name || user.username || "",
image: getUserAvatarUrl({
...user,
profile: user.profile,
avatarUrl: user.avatarUrl,
}),
theme: user.theme,
brandColor: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
@@ -183,20 +166,20 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
const safeBio = markdownToSafeHTML(user.bio) || "";
const markdownStrippedBio = stripMarkdown(user?.bio || "");
const org = usersWithoutAvatar[0].profile.organization;
const org = usersInOrgContext[0].profile.organization;
return {
props: {
users: users.map((user) => ({
users: usersInOrgContext.map((user) => ({
name: user.name,
username: user.username,
bio: user.bio,
avatarUrl: user.avatarUrl,
verified: user.verified,
profile: user.profile,
away: user.away,
})),
entity: {
...(org?.logoUrl ? { logoUrl: org?.logoUrl } : {}),
considerUnpublished: !isARedirectFromNonOrgLink && org?.slug === null,
orgSlug: currentOrgDomain,
name: org?.name ?? null,
@@ -3,7 +3,6 @@
import classNames from "classnames";
import type { InferGetServerSidePropsType } from "next";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { Toaster } from "react-hot-toast";
import {
@@ -23,7 +22,6 @@ import { type getServerSideProps } from "./users-public-view.getServerSideProps"
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
const searchParams = useSearchParams();
const [user] = users; //To be used when we only have a single user, not dynamic group
useTheme(profile.theme);
@@ -43,8 +41,6 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
...query
} = useRouterQuery();
const isRedirect = searchParams?.get("redirected") === "true" || false;
const fromUserNameRedirected = searchParams?.get("username") || "";
/*
const telemetry = useTelemetry();
useEffect(() => {
@@ -102,13 +98,13 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
{!isOrg && user.verified && (
<Icon
name="badge-check"
className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
className="mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
/>
)}
{isOrg && (
<Icon
name="badge-check"
className=" mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
className="mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
/>
)}
</h1>
@@ -125,47 +121,38 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
<div
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
data-testid="event-types">
{user.away ? (
<div className="overflow-hidden rounded-sm border ">
<div className="text-muted p-8 text-center">
<h2 className="font-cal text-default mb-2 text-3xl">😴{` ${t("user_away")}`}</h2>
<p className="mx-auto max-w-md">{t("user_away_description") as string}</p>
{eventTypes.map((type) => (
<div
key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
<Icon
name="arrow-right"
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
/>
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<div className="block w-full p-5">
<Link
prefetch={false}
href={{
pathname: `/${user.profile.username}/${type.slug}`,
query,
}}
passHref
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
data-testid="event-type-link">
<div className="flex flex-wrap items-center">
<h2 className="text-default pr-2 text-sm font-semibold">{type.title}</h2>
</div>
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
</Link>
</div>
</div>
) : (
eventTypes.map((type) => (
<div
key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
<Icon
name="arrow-right"
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
/>
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<div className="block w-full p-5">
<Link
prefetch={false}
href={{
pathname: `/${user.profile.username}/${type.slug}`,
query,
}}
passHref
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
data-testid="event-type-link">
<div className="flex flex-wrap items-center">
<h2 className=" text-default pr-2 text-sm font-semibold">{type.title}</h2>
</div>
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
</Link>
</div>
</div>
))
)}
))}
</div>
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
@@ -86,7 +86,6 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
booking,
user: usernames.join("+"),
slug,
away: false,
trpcState: ssr.dehydrate(),
isBrandingHidden: false,
isSEOIndexable: true,
@@ -165,7 +164,6 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
},
user: username,
slug,
away: false,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
isSEOIndexable: user?.allowSEOIndexing,
@@ -23,7 +23,6 @@ export default function Type({
user,
isEmbed,
booking,
away,
isBrandingHidden,
isSEOIndexable,
rescheduleUid,
@@ -47,7 +46,6 @@ export default function Type({
username={user}
eventSlug={slug}
bookingData={booking}
isAway={away}
hideBranding={isBrandingHidden}
entity={eventData.entity}
durationConfig={eventData.metadata?.multipleDuration}
@@ -0,0 +1,87 @@
import { useTranscription } from "@daily-co/daily-react";
import { useDaily, useDailyEvent } from "@daily-co/daily-react";
import React, { Fragment, useCallback, useRef, useState, useLayoutEffect, useEffect } from "react";
import { Toaster } from "react-hot-toast";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { showToast } from "@calcom/ui";
export const CalAiTransctibe = () => {
const daily = useDaily();
const { t } = useLocale();
const [transcript, setTranscript] = useState("");
const [transcriptHeight, setTranscriptHeight] = useState(0);
const transcriptRef = useRef<HTMLDivElement | null>(null);
const transcription = useTranscription();
useDailyEvent(
"app-message",
useCallback((ev) => {
const data = ev?.data;
if (data.user_name && data.text) setTranscript(`${data.user_name}: ${data.text}`);
}, [])
);
useDailyEvent("transcription-started", (ev) => {
showToast(t("transcription_enabled"), "success");
});
useDailyEvent("transcription-stopped", (ev) => {
showToast(t("transcription_stopped"), "success");
});
useDailyEvent("custom-button-click", (ev) => {
if (ev?.button_id !== "transcription") {
return;
}
if (transcription?.isTranscribing) {
daily?.stopTranscription();
} else {
daily?.startTranscription();
}
});
useLayoutEffect(() => {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
setTranscriptHeight(entry.target.scrollHeight);
}
});
if (transcriptRef.current) {
observer.observe(transcriptRef.current);
}
return () => observer.disconnect();
}, []);
useEffect(() => {
transcriptRef.current?.scrollTo({
top: transcriptRef.current?.scrollHeight,
behavior: "smooth",
});
}, [transcriptHeight]);
return (
<>
<Toaster position="bottom-right" />
<div
id="cal-ai-thing"
ref={transcriptRef}
className="max-h-full overflow-x-hidden overflow-y-scroll p-2 text-center text-white">
{transcript
? transcript.split("\n").map((line, i) => (
<Fragment key={`transcript-${i}`}>
{i > 0 && <br />}
{line}
</Fragment>
))
: ""}
</div>
</>
);
};
@@ -1,22 +1,27 @@
"use client";
import type { DailyCall } from "@daily-co/daily-js";
import DailyIframe from "@daily-co/daily-js";
import { DailyProvider } from "@daily-co/daily-react";
import Head from "next/head";
import { useState, useEffect, useRef } from "react";
import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { Icon } from "@calcom/ui";
import { CalAiTransctibe } from "~/videos/ai/ai-transcribe";
import { type PageProps } from "./videos-single-view.getServerSideProps";
export default function JoinCall(props: PageProps) {
const { t } = useLocale();
const { meetingUrl, meetingPassword, booking } = props;
const [daily, setDaily] = useState<DailyCall | null>(null);
useEffect(() => {
const callFrame = DailyIframe.createFrame({
@@ -42,8 +47,20 @@ export default function JoinCall(props: PageProps) {
},
url: meetingUrl,
...(typeof meetingPassword === "string" && { token: meetingPassword }),
customTrayButtons: {
transcription: {
label: "Cal.ai",
tooltip: "Enable transcription powered by AI",
iconPath: `${WEBAPP_URL}/sparkles.svg`,
iconPathDarkMode: `${WEBAPP_URL}/sparkles.svg`,
},
},
});
setDaily(callFrame);
callFrame.join();
return () => {
callFrame.destroy();
};
@@ -67,30 +84,35 @@ export default function JoinCall(props: PageProps) {
<meta property="twitter:title" content={`${APP_NAME} Video`} />
<meta property="twitter:description" content={t("quick_video_meeting")} />
</Head>
<div style={{ zIndex: 2, position: "relative" }}>
{booking?.user?.organization?.calVideoLogo ? (
<img
className="min-w-16 min-h-16 fixed z-10 hidden aspect-square h-16 w-16 rounded-full sm:inline-block"
src={booking.user.organization.calVideoLogo}
alt="My Org Logo"
style={{
top: 32,
left: 32,
}}
/>
) : (
<img
className="fixed z-10 hidden sm:inline-block"
src={`${WEBSITE_URL}/cal-logo-word-dark.svg`}
alt="Logo"
style={{
top: 32,
left: 32,
}}
/>
)}
</div>
<VideoMeetingInfo booking={booking} />
<DailyProvider callObject={daily}>
<div className="mx-auto" style={{ zIndex: 2, position: "absolute", bottom: 60, width: "100%" }}>
<CalAiTransctibe />
</div>
<div style={{ zIndex: 2, position: "relative" }}>
{booking?.user?.organization?.calVideoLogo ? (
<img
className="min-w-16 min-h-16 fixed z-10 hidden aspect-square h-16 w-16 rounded-full sm:inline-block"
src={booking.user.organization.calVideoLogo}
alt="My Org Logo"
style={{
top: 32,
left: 32,
}}
/>
) : (
<img
className="fixed z-10 hidden sm:inline-block"
src={`${WEBSITE_URL}/cal-logo-word-dark.svg`}
alt="Logo"
style={{
top: 32,
left: 32,
}}
/>
)}
</div>
<VideoMeetingInfo booking={booking} />
</DailyProvider>
</>
);
}
+1
View File
@@ -161,6 +161,7 @@ const nextConfig = {
experimental: {
// externalize server-side node_modules with size > 1mb, to improve dev mode performance/RAM usage
serverComponentsExternalPackages: ["next-i18next"],
optimizePackageImports: ["@calcom/ui"],
},
i18n: {
...i18n,
+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.9.8",
"version": "4.0.3",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -38,7 +38,8 @@
"@calcom/trpc": "*",
"@calcom/tsconfig": "*",
"@calcom/ui": "*",
"@daily-co/daily-js": "^0.37.0",
"@daily-co/daily-js": "^0.59.0",
"@daily-co/daily-react": "^0.17.2",
"@formkit/auto-animate": "1.0.0-beta.5",
"@glidejs/glide": "^3.5.2",
"@hookform/error-message": "^2.0.0",
@@ -121,6 +122,7 @@
"react-timezone-select": "^1.4.0",
"react-turnstile": "^1.1.3",
"react-use-intercom": "1.5.1",
"recoil": "^0.7.7",
"remove-markdown": "^0.5.0",
"rrule": "^2.7.1",
"sanitize-html": "^2.10.0",
+173
View File
@@ -0,0 +1,173 @@
import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
import { getDefaultScheduleId } from "@calcom/trpc/server/routers/viewer/availability/util";
const travelScheduleSelect = {
id: true,
startDate: true,
endDate: true,
timeZone: true,
prevTimeZone: true,
user: {
select: {
id: true,
timeZone: true,
defaultScheduleId: true,
},
},
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
if (process.env.CRON_API_KEY !== apiKey) {
res.status(401).json({ message: "Not authenticated" });
return;
}
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
}
let timeZonesChanged = 0;
const setNewTimeZone = async (timeZone: string, user: { id: number; defaultScheduleId: number | null }) => {
await prisma.user.update({
where: {
id: user.id,
},
data: {
timeZone: timeZone,
},
});
const defaultScheduleId = await getDefaultScheduleId(user.id, prisma);
if (!user.defaultScheduleId) {
// set default schedule if not already set
await prisma.user.update({
where: {
id: user.id,
},
data: {
defaultScheduleId,
},
});
}
await prisma.schedule.updateMany({
where: {
id: defaultScheduleId,
},
data: {
timeZone: timeZone,
},
});
timeZonesChanged++;
};
/* travelSchedules should be deleted automatically when timezone is set back to original tz,
but we do this in case there cron job didn't run for some reason
*/
const schedulesToDelete = await prisma.travelSchedule.findMany({
where: {
OR: [
{
startDate: {
lt: dayjs.utc().subtract(2, "day").toDate(),
},
endDate: null,
},
{
endDate: {
lt: dayjs.utc().subtract(2, "day").toDate(),
},
},
],
},
select: travelScheduleSelect,
});
for (const travelSchedule of schedulesToDelete) {
if (travelSchedule.prevTimeZone) {
await setNewTimeZone(travelSchedule.prevTimeZone, travelSchedule.user);
}
await prisma.travelSchedule.delete({
where: {
id: travelSchedule.id,
},
});
}
const travelSchedulesCloseToCurrentDate = await prisma.travelSchedule.findMany({
where: {
OR: [
{
startDate: {
gte: dayjs.utc().subtract(1, "day").toDate(),
lte: dayjs.utc().add(1, "day").toDate(),
},
},
{
endDate: {
gte: dayjs.utc().subtract(1, "day").toDate(),
lte: dayjs.utc().add(1, "day").toDate(),
},
},
],
},
select: travelScheduleSelect,
});
const travelScheduleIdsToDelete = [];
for (const travelSchedule of travelSchedulesCloseToCurrentDate) {
const userTz = travelSchedule.user.timeZone;
const offset = dayjs().tz(userTz).utcOffset();
// midnight of user's time zone in utc time
const startDateUTC = dayjs(travelSchedule.startDate).subtract(offset, "minute");
// 23:59 of user's time zone in utc time
const endDateUTC = dayjs(travelSchedule.endDate).subtract(offset, "minute");
if (
!dayjs.utc().isBefore(startDateUTC) &&
dayjs.utc().isBefore(endDateUTC) &&
!travelSchedule.prevTimeZone
) {
// if travel schedule has started and prevTimeZone is not yet set, we need to change time zone
await setNewTimeZone(travelSchedule.timeZone, travelSchedule.user);
if (!travelSchedule.endDate) {
travelScheduleIdsToDelete.push(travelSchedule.id);
} else {
await prisma.travelSchedule.update({
where: {
id: travelSchedule.id,
},
data: {
prevTimeZone: travelSchedule.user.timeZone,
},
});
}
}
if (!dayjs.utc().isBefore(endDateUTC)) {
if (travelSchedule.prevTimeZone) {
// travel schedule ended, change back to original timezone
await setNewTimeZone(travelSchedule.prevTimeZone, travelSchedule.user);
}
travelScheduleIdsToDelete.push(travelSchedule.id);
}
}
await prisma.travelSchedule.deleteMany({
where: {
id: {
in: travelScheduleIdsToDelete,
},
},
});
res.status(200).json({ timeZonesChanged });
}
@@ -286,7 +286,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
in: userIds as number[],
},
},
select: { id: true, name: true, email: true, avatar: true, username: true },
select: { id: true, name: true, email: true, avatarUrl: true, username: true },
});
const userHashMap = new Map();
+1
View File
@@ -12,6 +12,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
res.write(
await renderEmail("MonthlyDigestEmail", {
language: t,
+3 -3
View File
@@ -6,7 +6,7 @@ import { z } from "zod";
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
@@ -15,7 +15,7 @@ import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
const log = logger.getSubLogger({ prefix: ["recorded-daily-video"] });
const log = logger.getSubLogger({ prefix: ["daily-video-webhook-handler"] });
const schema = z
.object({
@@ -116,7 +116,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const response = schema.safeParse(req.body);
log.debug(
"Recording Request Body:",
"Daily video recording webhook Request Body:",
safeStringify({
response,
})
@@ -0,0 +1,182 @@
import { createHmac } from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient";
import { sendDailyVideoTranscriptEmails } from "@calcom/emails";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { defaultHandler } from "@calcom/lib/server";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
const testRequestSchema = z.object({
test: z.enum(["test"]),
});
const log = logger.getSubLogger({ prefix: ["send-daily-video-transcript-handler"] });
const schema = z
.object({
version: z.string(),
type: z.string(),
id: z.string(),
payload: z
.object({
meeting_id: z.string(),
end_ts: z.number().optional(),
room: z.string(),
start_ts: z.number().optional(),
})
.passthrough(),
event_ts: z.number().optional(),
})
.passthrough();
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (testRequestSchema.safeParse(req.body).success) {
return res.status(200).json({ message: "Test request successful" });
}
const hmacSecret = process.env.DAILY_MEETING_ENDED_WEBHOOK_SECRET;
if (!hmacSecret) {
return res.status(405).json({ message: "No Daily Webhook Secret" });
}
const signature = `${req.headers["x-webhook-timestamp"]}.${JSON.stringify(req.body)}`;
const base64DecodedSecret = Buffer.from(hmacSecret, "base64");
const hmac = createHmac("sha256", base64DecodedSecret);
const computed_signature = hmac.update(signature).digest("base64");
if (req.headers["x-webhook-signature"] !== computed_signature) {
return res.status(403).json({ message: "Signature does not match" });
}
const response = schema.safeParse(req.body);
log.debug(
"Daily video transcript webhook Request Body:",
safeStringify({
response,
})
);
if (!response.success || response.data.type !== "meeting.ended") {
return res.status(400).send({
message: "Invalid Payload",
});
}
const { room, meeting_id } = response.data.payload;
try {
const bookingReference = await prisma.bookingReference.findFirst({
where: { type: "daily_video", uid: room, meetingId: room },
select: { bookingId: true },
});
if (!bookingReference || !bookingReference.bookingId) {
log.error(
"bookingReference Not found:",
safeStringify({
bookingReference,
requestBody: req.body,
})
);
return res.status(404).send({ message: "Booking reference not found" });
}
const booking = await prisma.booking.findUniqueOrThrow({
where: {
id: bookingReference.bookingId,
},
select: {
...bookingMinimalSelect,
uid: true,
location: true,
isRecorded: true,
eventTypeId: true,
eventType: {
select: {
teamId: true,
parentId: true,
},
},
user: {
select: {
id: true,
timeZone: true,
email: true,
name: true,
locale: true,
destinationCalendar: true,
},
},
},
});
if (!booking) {
log.error(
"Booking Not Found:",
safeStringify({
booking,
requestBody: req.body,
})
);
return res.status(404).send({
message: `Booking of room_name ${room} does not exist or does not contain daily video as location`,
});
}
const transcripts = await getAllTranscriptsAccessLinkFromRoomName(room);
if (!transcripts || !transcripts.length)
return res.status(200).json({ message: `No Transcripts found for room name ${room}` });
const t = await getTranslation(booking?.user?.locale ?? "en", "common");
const attendeesListPromises = booking.attendees.map(async (attendee) => {
return {
id: attendee.id,
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
};
});
const attendeesList = await Promise.all(attendeesListPromises);
// Send emails
const evt: CalendarEvent = {
type: booking.title,
title: booking.title,
description: booking.description || undefined,
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
organizer: {
email: booking?.userPrimaryEmail || booking.user?.email || "Email-less",
name: booking.user?.name || "Nameless",
timeZone: booking.user?.timeZone || "Europe/London",
language: { translate: t, locale: booking?.user?.locale ?? "en" },
},
attendees: attendeesList,
uid: booking.uid,
};
await sendDailyVideoTranscriptEmails(evt, transcripts);
return res.status(200).json({ message: "Success" });
} catch (err) {
console.error("Error in /send-daily-video-transcript", err);
return res.status(500).json({ message: "something went wrong" });
}
}
export default defaultHandler({
POST: Promise.resolve({ default: handler }),
});
-154
View File
@@ -1,154 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import {
orgDomainConfig,
whereClauseForOrgWithSlugOrRequestedSlug,
} from "@calcom/features/ee/organizations/lib/orgDomains";
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
const log = logger.getSubLogger({ prefix: ["team/[slug]"] });
const querySchema = z
.object({
username: z.string(),
teamname: z.string(),
/**
* Passed when we want to fetch avatar of a particular organization
*/
orgSlug: z.string(),
/**
* Allow fetching avatar of a particular organization
* Avatars being public, we need not worry about others accessing it.
*/
orgId: z.string().transform((s) => Number(s)),
})
.partial();
async function getIdentityData(req: NextApiRequest) {
const { username, teamname, orgId, orgSlug } = querySchema.parse(req.query);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req);
const org = isValidOrgDomain ? currentOrgDomain : null;
const orgQuery = orgId
? {
id: orgId,
}
: org
? whereClauseForOrgWithSlugOrRequestedSlug(org)
: null;
if (username) {
const users = await prisma.user.findMany({
where: {
...(orgQuery
? {
profiles: {
some: {
username,
organization: orgQuery,
},
},
}
: {
username,
// If a user is moved, it isn't actually available outside of the organization. So, for non-org domain check for movedToProfileId
movedToProfileId: null,
}),
},
select: { avatar: true, email: true },
});
if (users.length > 1) {
throw new Error(`More than one user found for username "${username}"`);
}
const [user] = users;
return {
name: username,
email: user?.email,
avatar: user?.avatar,
org,
};
}
if (teamname) {
const team = await prisma.team.findFirst({
where: {
slug: teamname,
parent: orgQuery,
},
select: { logo: true },
});
return {
org,
name: teamname,
email: null,
avatar: getPlaceholderAvatar(team?.logo, teamname),
};
}
if (orgSlug) {
const orgs = await prisma.team.findMany({
where: {
...whereClauseForOrgWithSlugOrRequestedSlug(orgSlug),
},
select: {
slug: true,
logo: true,
name: true,
},
});
if (orgs.length > 1) {
// This should never happen, but instead of throwing error, we are just logging to be able to observe when it happens.
log.error("More than one organization found for slug", orgSlug);
}
const org = orgs[0];
return {
org: org?.slug,
name: org?.name,
email: null,
avatar: getPlaceholderAvatar(org?.logo, org?.name),
};
}
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const identity = await getIdentityData(req);
const img = identity?.avatar;
// If image isn't set or links to this route itself, use default avatar
if (!img) {
if (identity?.org) {
res.setHeader("x-cal-org", identity.org);
}
res.writeHead(302, {
Location: AVATAR_FALLBACK,
});
return res.end();
}
if (!img.includes("data:image")) {
if (identity.org) {
res.setHeader("x-cal-org", identity.org);
}
res.writeHead(302, { Location: img });
return res.end();
}
const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", "");
const imageResp = Buffer.from(decoded, "base64");
if (identity.org) {
res.setHeader("x-cal-org", identity.org);
}
res.writeHead(200, {
"Content-Type": "image/png",
"Content-Length": imageResp.length,
});
res.end(imageResp);
}
+5 -10
View File
@@ -1,8 +1,8 @@
import type { GetStaticPropsContext } from "next";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import z from "zod";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon } from "@calcom/ui";
@@ -17,11 +17,9 @@ const querySchema = z.object({
export default function Error() {
const { t } = useLocale();
const searchParams = useCompatSearchParams();
const { error } = querySchema.parse(searchParams);
const isTokenVerificationError = error?.toLowerCase() === "verification";
const errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
const searchParams = useSearchParams();
const { error } = querySchema.parse({ error: searchParams?.get("error") || undefined });
const errorMsg = error || t("error_during_login");
return (
<AuthContainer title="" description="">
<div>
@@ -30,11 +28,8 @@ export default function Error() {
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
{error}
{errorMsg}
</h3>
<div className="mt-2">
<p className="text-subtle text-sm">{errorMsg}</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-6">
+5 -5
View File
@@ -25,15 +25,15 @@ function VerifyEmailChange(props: PageProps) {
const router = useRouter();
useEffect(() => {
async function updateSession() {
async function updateSessionAndDisplayToast() {
await update({ email: props.updatedEmail });
router.push("/event-types");
}
if (props.updateSession) {
updateSession();
if (isLocaleReady) {
showToast(t("verify_email_change_success_toast", { email: props.updatedEmail }), "success");
}
router.push("/event-types");
}
if (props.updateSession) {
updateSessionAndDisplayToast();
} else {
if (isLocaleReady) {
showToast(t("verify_email_change_failure_toast"), "error");
+1 -1
View File
@@ -24,7 +24,7 @@ import SkeletonLoader from "@components/availability/SkeletonLoader";
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
const { t } = useLocale();
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
const utils = trpc.useContext();
const utils = trpc.useUtils();
const meQuery = trpc.viewer.me.useQuery();
+19 -3
View File
@@ -1,16 +1,32 @@
import { Troubleshooter } from "@calcom/features/troubleshooter/Troubleshooter";
import dynamic from "next/dynamic";
import { Suspense } from "react";
import { getLayout } from "@calcom/features/troubleshooter/layout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui";
import { HeadSeo, Loader } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
const TroubleshooterClientOnly = dynamic(
() => import("@calcom/features/troubleshooter/Troubleshooter").then((mod) => mod.Troubleshooter),
{
ssr: false,
}
);
function TroubleshooterPage() {
const { t } = useLocale();
return (
<>
<HeadSeo title={t("troubleshoot")} description={t("troubleshoot_availability")} />
<Troubleshooter month={null} />
<Suspense
fallback={
<div className="flex h-full w-full items-center justify-center">
<Loader />
</div>
}>
<TroubleshooterClientOnly month={null} />
</Suspense>
</>
);
}
-2
View File
@@ -13,7 +13,6 @@ export default function Type({
isEmbed,
user,
booking,
away,
isBrandingHidden,
isTeamEvent,
entity,
@@ -33,7 +32,6 @@ export default function Type({
username={user}
eventSlug={slug}
bookingData={booking}
isAway={away}
hideBranding={isBrandingHidden}
isTeamEvent={isTeamEvent}
entity={entity}
+5 -13
View File
@@ -3,31 +3,23 @@
import Head from "next/head";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
export default function MaintenancePage() {
const { t, isLocaleReady } = useLocale();
if (!isLocaleReady) return null;
return (
<div className="bg-subtle flex h-screen">
<Head>
<title>
{t("under_maintenance")} | {APP_NAME}
</title>
<title>Under Maintenance | {APP_NAME}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="bg-default m-auto rounded-md p-10 text-right ltr:text-left">
<h1 className="text-emphasis text-2xl font-medium">{t("under_maintenance")}</h1>
<h1 className="text-emphasis text-2xl font-medium">Down for maintenance</h1>
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">
{t("under_maintenance_description", { appName: APP_NAME })}
The Cal.com team are performing scheduled maintenance. If you have any questions, please contact
support.
</p>
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
<Button href={`${WEBSITE_URL}/support`}>Contact Support</Button>
</div>
</div>
);
}
MaintenancePage.PageWrapper = PageWrapper;
@@ -21,7 +21,6 @@ export default function Type({
slug,
user,
booking,
away,
isEmbed,
isBrandingHidden,
entity,
@@ -42,7 +41,6 @@ export default function Type({
username={user}
eventSlug={slug}
bookingData={booking}
isAway={away}
hideBranding={isBrandingHidden}
isTeamEvent
isInstantMeeting
@@ -110,7 +108,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
eventData.length
),
booking: null,
away: false,
user: teamSlug,
teamId: team.id,
slug: meetingSlug,

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