Compare commits

..
245 Commits
Author SHA1 Message Date
Omar López 816d31918d chore: v4.0.8 (#14965)
release.yml / release (push) Canceled after 0s
2024-05-09 13:20:59 -07:00
Ayush MhetreandCarina Wollendorfer 097a69d7d7 fix: save workflow button not visible on dev env (#14910)
* fix: save workflow button not visible on dev env

* refactor: use the CALCOM_ENV constant

* fix: ui fix for header on workspace pages

* refactor: remove unused variable

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-05-09 17:46:39 +00:00
c2a07e221d feat: platform onboarding flow and dashboard (#14721)
* add endpoint to fetch managed user from client id

* update typings

* minor tweaks

* custom hook to fetch managed users from client id

* add translations for platform onboarding

* add isPlatformOrg boolean to figure out which is platform and which is not

* set isPlatform hook based on data obtained from org

* add limitWidth prop to control component width

* add props to shell to make sidebar display different tabs based on if the shell isPlattform or not

* platform related pages

* fix merge conflicts

* fix merge conflicts

* platform oauth client form and card

* remove everything related to platform from organization

* update oauth client card and form

* fixup

* fix imports and remove logs

* fixup

* update redirect url

* split oauth client form into separate update and create forms

* separate forms for create and edit oauth clients

* fixup

* fixup

* dynamic routes for oauth client edit page

* fixup fixup

* fix to not show error when redirect uri is empty

* refactor create handler for org

* cleaup comments

* add custom hook to check user billing

* export managed user type

* refactor platform index page

* refactor edit and create pages

* dashboard component containing oauth client list and managed user

* common oauth client form used for create and edit form

* platform pricing helper

* platform pricing component

* fix typing and data response for billing

* use custom hook to check team billing info

* fix type checks

* upgrade conditional rendering for upgrade to org banner

* add isLoading prop to check button loading state

* pass in button handler

* add custom hook to subscribe to stripe and typings

* update typings

* fix incorrect endpoint

* pass in team id as prop

* fix type check

* update stripe success and cancel redirect url

* add and pass redirect url param to custom hook

* custom hooks for platform

* cleanup

* update imports

* fix merge conflicts

* fixup

* fixup fixup

* fixup

* merge conflicts fixup

* merge conlficts battle :(

* minor fixes

* skip admin checks for a platform client

* fix typo

* append slug with _platform for a platform user

* PR feedback

* dashboard refactor

* bring back org form to its orginal state

* add platform folder to ee

* update typings

* use new create platform form

* fixup

* fix typo and update plans

* simplifying rendering

* remove managed users endpoint since it already exists

* url for endpoint

* rename tabs

* pr feedback

* managed users endpoint

* update endpoint

* remove form

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: exception <erik@erosemberg.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-05-09 13:01:10 -04:00
Morgan 6d8fbfc49a fix: gcal save get credentials from token.tokens (#14962) 2024-05-09 14:08:31 +00:00
Carina WollendorferandCarinaWolli 304ef29618 fix: buffer times in handleNewBooking (#14871)
* get buffer busy time from bookings outside of start and end time

* add test for buffers

* fix typo

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-05-09 13:39:32 +00:00
Lauris Skraucis b8890c841c fix: managed user timeZone, timeFormat and weekStart during creation (#14955)
* refactor: create, update user inputs use types instead of custom validators

* fix: save weekStart, timeFormat, timeZone when creating user

* fix: save weekStart, timeFormat, timeZone when creating user

* e2e: fix env variables and test weekStart, timeZone and timeFormat during user create
2024-05-09 10:25:14 -03:00
Amit SharmaandKeith Williams 2f5768b74f fix: Only able to create one webhook per event type (#14922)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-05-09 02:40:13 +00:00
Carina WollendorferandCarinaWolli c1416173e8 attempt to fix flaky e2e test (#14949)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-05-08 18:07:30 -04:00
Morgan 726af4970c fix: sync book event form values with bookingData (#14945)
* fix: sync book event form values with bookingData

* fixup! fix: sync book event form values with bookingData
2024-05-08 15:14:40 +00:00
Keith Williams d15b7d8168 chore: Update PR template to include more mandatory tasks (#14941)
* chore: Update PR template to include more mandatory tasks

* Add XXXX to GitHub number

* Update PULL_REQUEST_TEMPLATE.md
2024-05-08 14:37:17 +01:00
Alex van Andel c5bd45fdfb feat: Redirect when event type does not match booking type (#14460)
* feat: Redirect when event type does not match booking type

* Favour early return

* If multiple bookingUid/rescheduleUid given, use first

* rescheduleUid/bookingUid behaviour is required
2024-05-08 10:33:22 -03:00
Morgan 5d7b2a12a7 fix: redis cache key overlap (#14938) 2024-05-08 10:41:34 +00:00
Morgan 7c90d1ed76 feat: handleCreateBooking for platform booker (#14939) 2024-05-08 16:11:01 +05:30
Peer Richelsen 5ca10d1e83 feat: Added admin-api page (#14890)
* added admin-api page

* i18n
2024-05-08 10:15:20 +00:00
sean-brydon f0e59c2331 fix: pass query params to dynamic meetings (#14920) 2024-05-08 09:53:07 +00:00
Alex van AndelandKeith Williams f812d92045 chore: Release/v4.0.7 (#14933)
release.yml / release (push) Canceled after 0s
* chore: release v4.0.7

* fix: Remove lockfile, could be broken

* fix: Bump lockfile (again)

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-05-08 00:17:51 +01:00
Crowdin Bot 23a17c4e8a New Crowdin translations by Github Action 2024-05-07 22:24:57 +00:00
Keith Williams 5beb039003 fix: Caching key for CI database (#14932)
* fix: Caching key for db

* Fix line length
2024-05-07 22:21:39 +00:00
Keith Williams 63dd357664 chore: Removed duplicate production build (#14836)
* chore: Removed duplicate production build

* fix references

* Move env vars to top level of e2e

* job renaming

* Added env vars to top level of all e2e jobs

* Removed part of cache key that causes issues and is moot

* Using buildjet hardware so the caching works

* clean up
2024-05-07 12:56:19 -07:00
b6a57381c7 feat: redirect after booking: option to forward parameters (#14235)
* feat: redirect after booking: option to forward parameters

* fix: unit test

* fix: stripe passing its own params

* fix: manually add stripe params

---------

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-05-07 12:15:20 -04:00
Alex van Andel 7cf840723b Async scalars & less queries due to grouping (#14904) 2024-05-07 15:18:07 +01:00
sean-brydonandUdit Takkar bc0c40d260 feat: cal-3452 parameter for impersonation (#14605)
* feat: impersonate trigger when there is a URL param

* feat: update intercom sidebar

* fix: add to onOpen too

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-05-07 10:33:19 -03:00
MorganandKeith Williams 89612a7a68 fix: add prisma client generated types to atoms package (#14916)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-05-07 12:59:41 +00:00
a2620a0e50 feat: atoms team booking (#14525)
* feat: setup usePublicEvent with orgSlug and duration

* feat: setup useAvailableSlots with eventTypeSlug and orgSlug

* fix & feat: fix TS issues and allow passing multiple users to Booker

* refactor: dont show [15min] [30min] [60min] [1h30min] picker

* fix: pass orgSlug to handleNewBooking -> loadUsers -> findUsersByUsername to not return empty users[]

* refactor: display attendees in booking confirmation screen

* fix: getting slots get orgSlug from props not event

* revert: Booker username use props instead of hardcoded values

* refactor: setup BookerStore org and use it for selectedTime + isDynamic logic

* refactor: hide 'what is this meeting about' input in final booking step

* fix: TS error

* revert: what is the meeting about field hiding

* revert: make org as org? in booker store

* revert: createNewBooking -> loadUsers -> get forced orgSlug from header not body

* refactor: useHandleBookEvent get orgSlug from booker store not props

* fix: if entity undefined dont access orgSlug

* refactor: add isDynamic prop as queryKey for usePublicEvent

* refactor: remove unused prop

* re-add docs

* fix: typescript error

* fix: force platform orgSlug in handleNewBooking.ts

* fix: remove duplicate setSelectedDuration declaration

* refactor: destructure properies from body instead of req.body

* fix: entity optional and hide event members

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-05-07 12:51:00 +00:00
sean-brydon cfcf5b168c fix: slot check (#14917) 2024-05-07 12:43:10 +00:00
3f4d9962a7 fix: reschedule with google meet (#14730)
* fix: reschedule with google meet

* fix: type err

* test: add google meet test

* chore: save progress

* test: add individual event type

* chore: add one more check

* fix: type err

* chore: use updatedEvent before createdEvent

* refactor: update event

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-05-07 18:09:12 +05:30
Keith Williams 83917e8655 chore: Add build of API v2 to PR checks (#14902)
* chore: Add build of API v2 to PR checks

* Actually added the v2 build to the pipeline...

* Added back the original yarn install

* Updated the main PR flow to require API builds before tests
2024-05-07 09:05:49 -03:00
Carina WollendorferandCarinaWolli f4bd0dbcc9 remove global variable (#14907)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-05-07 11:04:27 +01:00
c87b8b43f9 Add resourceOwner.id related test (#14816)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-05-06 23:50:59 +00:00
shaik-zeeshanandJoe Au-Yeung b2baaea84c feat: "Add an Override" calendar starts with the day specified in general settings (#14663)
* feat: Add an Override calendar starts with the day specified in general settings

* fix: types not matching

* fix: update edit calendar to use user's weekStart

* fix: weekstart defaults to 0 when not provided

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-05-06 19:00:03 -03:00
Vikas PatilandJoe Au-Yeung 0e431ba7c6 feat: posthog app added (#14801)
* feat: posthog app added

* fix:requested changes

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-05-06 15:54:53 -03:00
sean-brydon b62f6eee3c fix: date override modal (#14854) 2024-05-06 18:34:17 +00:00
Peer Richelsen 2d0cac0dad fix: windows app styling (#14805) 2024-05-06 17:41:45 +00:00
Morgan 0c3d1b3034 chore: bump atoms v1.0.40 (#14901) 2024-05-06 17:31:08 +00:00
74f6d7686b fix: zod error message and missing translation (#14787)
* fix: issue#14785 and issue#14786

* fix: some changes

* fix

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-05-06 16:03:24 +00:00
Amit SharmaandKeith Williams 49601bed6f fix: Sender ID info not being shown on 'info' hover in 'add action' flow (#14879)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-05-06 12:16:03 -03:00
ErikandMorgan b9659051d4 chore: Cache cityTimezones in Platform (#14823)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-05-06 12:00:36 -03:00
Peer RichelsenandKeith Williams 9c96507af2 chore: centered cal video logo (#14896)
* centered cal video logo

* Update apps/web/modules/videos/views/videos-single-view.tsx

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-05-06 14:25:14 +00:00
Utkersh Rajvenshi 2352babe93 fix: UI breaking in AddActionDialog on reducing browser window height (#14892) 2024-05-06 14:24:45 +00:00
Keith Williams 1467815c3f chore: Use 4vCPU BuildJet hardware for type checks (#14900) 2024-05-06 10:31:37 -03:00
7254cd97e2 chore: Use better buildjet hardware for e2e (#14835)
* chore: Use better buildjet server for e2e

* Backed down buildjet hardware where not needed

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-05-06 12:27:14 +00:00
Morgan 4cb77abfd7 fix: api v2 docs in prod (#14898) 2024-05-06 11:50:21 +00:00
8deedfd361 fix: Booking Cancelled Webhook - Organiser webhook payload is missing id. #14697 (#14745)
* fix: include id information to be consistent with create/reschedule flows

* fix: include username as well

* function expectBookingCancelledWebhookToHaveBeenFired created

* chore: undo yarn.lock

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-05-06 08:14:09 +00:00
3a0c766ed5 fix: Support adding a non-existent email as the owner of the organization (#14569)
* fix: Support adding an non-existent email as the owner of the organization

* Remove dead code

* fixes

* add test

* self review changes

* Update packages/trpc/server/routers/viewer/organizations/create.handler.ts

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

* Fix typo everywhere

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-05-06 07:09:22 +00:00
Hariom Balhara 4f57e3201d test: Google CalendarService test (#14793) 2024-05-06 12:33:16 +05:30
MOHD RAZA e404f7eb88 UI breaking in AddActionDialog fixed (#14883) 2024-05-06 07:02:43 +00:00
5c2b561a5c fix webhook e2e test (#14850)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-05-03 14:59:52 +00:00
Amit Sharma 57b404f721 feat: Add reschedule information with booking Read (#14853) 2024-05-03 14:31:04 +00:00
Amit SharmaandCarina Wollendorfer df9a6621cf fix: Sender ID info not shown on hover (#14846)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-05-03 14:08:09 +00:00
c478f73327 feat: orgs trigger alert when loading a calendar and no availability is (#14796)
* Install upstash redis again - base of noficationSender

* Setup email handler etc and use tasker

* Remove logs

* Sending emails and correct format and spacing in emails

* Update email styles

* Update email styles

* add switch to enable feature

* fix: reset tasker types - will fix in new PR

* WIP: test suite

* WIP: test suite

* Add redis service and add WIP mock test

* Add tests for redis service and fix lpush

* More working tests

* More working tests

* fix: type error + typo

* update export to match i18n next mock

* (debug) push for debug

* Fix: fixed hosting in mocks

* Add common.json i18n

* add better test descriptions

* reset mocks after each test to allow concurancy

* Remove redundant RedisService.test.ts

* Rename and remove redundant isAdmin check

* Rename to .d.ts

* Add key versioning to constructRedisKey

* Update packages/trpc/server/routers/viewer/slots/handleNotificationWhenNoSlots.ts

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

* Update packages/trpc/server/routers/viewer/slots/handleNotificationWhenNoSlots.ts

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

* Add try/catch

* fix breaking when option === undefined

* Add missing await to Promise.all

* Rename data stored in redis to save space

* Include thrown error in logs

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-05-03 13:48:04 +00:00
Hariom Balhara 88f4dd2446 Ensure that availability is fetched in parallel (#14809) 2024-05-03 13:56:10 +01:00
Keith Williams 97b2816897 v4.0.6 (#14865)
release.yml / release (push) Canceled after 0s
2024-05-02 22:23:46 +00:00
Erik 00372b6b3b chore: Discard unused Platform stripe events (#14822)
* chore: discard unused webhooks

* chore: discard early
2024-05-02 16:31:50 +00:00
Hariom Balhara 6c8fced247 fix: Snippet path in preview (#14830)
* fix: Snippet path in preview

* Add test
2024-05-02 16:03:57 +00:00
Erik 52813b01e6 chore: don't try to increase usage for legacy plans (#14856) 2024-05-02 15:24:42 +00:00
Erik 77a98d96c8 chore: attach unique request ids (#14857) 2024-05-02 15:24:24 +00:00
Peer RichelsenandUdit Takkar 676eac357f chore: fixed enterprise warning and description (#14791)
* fixed enterprise warning and description

* chore: remove fullstop

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-05-02 15:14:13 +00:00
Joe Au-Yeung 5bafd1b264 chore: Allow disabling attendee emails if SMS_ATTENDEE is a part of the workflow (#14806)
* Allow disabling confirmation emails to attendees if workflow contains SMS

* Update copy
2024-05-02 10:44:08 -04:00
0391bfe887 test: add unit tests for sms sending (#14737)
* add tests for workflow sms

* fix type error

* improvements

* add tests for team workflow

* rename test

* add emailsToReceive everywhere

* fix type errors

* code clean up

* fix fresh-booking.test.ts

* add destination email to emailsToReceive

* remove unused webhooks

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-05-02 10:42:38 -04:00
Peer Richelsen c18407d75a full screen ratings (#14839) 2024-05-02 09:43:28 +00:00
Erik a631286770 chore: Add default to match migration for Platform Billing (#14847) 2024-05-02 05:29:32 +00:00
Carina WollendorferandCarinaWolli dd78741158 fix e2e test (#14845)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-05-01 20:45:40 +00:00
Peer Richelsen 8f69811e25 chore: Update README.md (#14819) 2024-05-01 21:18:53 +02:00
Erik 3a63a8b228 chore: Bring back next-18next.config.js (#14841)
* chore: bring back next-18next.config.js

* chore: bring back next-18next.config.js
2024-05-01 10:23:03 -03:00
Somay ChauhanandPeer Richelsen ef98105503 feat: send organization_id to intercom (#14811)
* send organization_id to intercom

* send org slug instead of org id

* fix: type errors

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-04-30 21:38:18 +00:00
Erik a18f1eb382 v4.0.5 (#14820)
release.yml / release (push) Canceled after 0s
2024-04-30 21:23:51 +00:00
Joe Au-Yeung 2ab6315a8e Add two more shards to E2E pipeline (#14818) 2024-04-30 20:31:17 +00:00
Joe Au-Yeungandexception 258ebe0420 chore: Update yarn.lock file (#14812)
* Update yarn.lock file

* chore: update lockfile

---------

Co-authored-by: exception <erik@erosemberg.com>
2024-04-30 20:05:29 +00:00
Erik 1cfd2f7053 feat: Billing Service (#14756)
* chore: Billing Service

* chore: others

* chore: don't type

* chore: typo

* chore: use proper wh secret

* chore: updates

* chore: lockfile

* chore: docs and comment out

* chore: updates and include plan

* chore: config

* chore: bring back alias

* chore: update lockfile

* chore: refactor

* chore: default none plan in db

* chore: read from env vars

* chore: feedback

* chore: remove json config

* chore: proper type
2024-04-30 20:02:04 +00:00
Hariom Balhara a51024e915 hotfix-conditional-valid-resource-owner-check (#14814) 2024-04-30 15:05:00 +00:00
Hariom BalharaandJoe Au-Yeung 900e90cd2c fix: With Credential Sync enabled, fix expired google calendar token handling - Refresh wasn't working (#14788)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-04-29 20:37:18 +00:00
Somay Chauhan 46ffeaee82 feat: add additional attributes to intercom (#14794)
* feat: add additional attributes to intercom

* remove console.log

* added is_premium
2024-04-29 19:50:07 +02:00
Alex van Andel 22425bb693 Revert "fix: invalid Cancelled / Rescheduled downstream calendar block (#14700)" (#14800)
This reverts commit 0306ad737f.
2024-04-29 13:31:02 -03:00
Syed Ali Shahbaz ad0b8a668c fix: Unbork Team POST API (#14803)
* Add newly added mandatory fields to partial

* --WIP
2024-04-29 17:10:10 +01:00
bbad2c9d31 fix: v2 ts config and errors (#14799)
* fix: exclude platform examples

* fix: v2 event-types TS errors

* fix: nest entry file

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-04-29 16:06:25 +00:00
Amit SharmaandUdit Takkar 8b28a6f5df feat: when creating workflow from within event-type, preselect it (#14774)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-29 14:23:28 +00:00
Chris 93bb37702c feat: Add app store entry for Clic (CheckIn application) (#14754)
* feat: create app

* feat: create app

* feat: initial commit
2024-04-29 13:26:13 +00:00
Alex van Andel 4a8f251d89 fix: #14769 (#14777) 2024-04-27 18:14:37 +02:00
lambertrocherandlambert cf44919cdd fix missing french translation for keys round_robin_emailed_you_and_attendees and round_robin_emailed_you_and_attendees_recurring (#14773)
Co-authored-by: lambert <lambert.rocher@jellysmack.com>
2024-04-27 01:19:56 +00:00
Morgan 024e40b26b chore: api v2 tsconfig manual path to internal packages (#14771) 2024-04-26 20:40:30 -03:00
Nishan SinghandKeith Williams 7c354321be feat: Add linting rule to recursively check for Prisma includes using… (#13652)
* feat: Add linting rule to recursively check for Prisma includes using only 'true'

* Fixing linting errors

* More linting fixes

* Disabled linting for forms

* Update recommended.ts

* Removed ignores

* Fixed warning

* Type fix

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-04-26 21:16:16 +00:00
Abhiuday Gupta fc16ec59d3 fix: confirm booking doesn't respect minimum booking notice (#14577)
Signed-off-by: Abhiuday <contact.abhiuday@gmail.com>
2024-04-26 11:58:11 -04:00
Hariom Balhara ab26481738 fix child element click popup (#14749) 2024-04-26 14:37:27 +00:00
Amit SharmaandUdit Takkar 2fbc06314e feat: Hide recurring tab when payments are enabled (#14731)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-26 10:18:29 -04:00
sean-brydonandUdit Takkar 386006fad0 feat: date range picker (#14713)
* feat: add DateRangePicker.tsx and Calendar.tsx to match DS

* chore:cleanup

* tests: remove as these are duplicate tests from the library themselves

* fix: mobile sizes

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-26 13:38:42 +00:00
ceb555460e feat: Platform Rate limiting + access token caching (#14560)
* feat: Platform Rate limiting + access token caching

* chore: changes

* chore: cleanup

* chore: remove yarnlock changes

* chore: yarnlcok

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-04-26 13:14:13 +00:00
Lauris Skraucis c28870e029 refactor: booker atom unmount reset duration query param (#14766) 2024-04-26 14:01:50 +03:00
Syed Ali Shahbaz 0650898302 feat: Add ratings to insights (#14687) 2024-04-26 11:58:06 +01:00
Lauris Skraucis d004815a68 feat: v2 managed user create default schedule given timeZone (#14727)
* schedules service: function to create default user schedule

* managed user: create default schedule if timeZone provided

* swagger update
2024-04-26 09:20:29 +00:00
Morgan 5d76bd6b9c v4.0.4 (#14764)
release.yml / release (push) Canceled after 0s
2024-04-26 09:21:07 +03:00
Erik d02947f887 chore: SDK cleanup (#14763) 2024-04-25 23:22:47 +00:00
Erik a3a95a1496 chore: SDK readme (#14761) 2024-04-25 20:26:46 +00:00
Hariom BalharaandJoe Au-Yeung d47c6b3fdb fix: Credential Syncing Improvements (#14588)
* Add example app to test credential sync

* Fixes

* Changes to normalize flow of GoogleCalendar and Zoom

* Add unit tests

* PR Feedback

* credential-sync-more-tests-and-more-apps

* Fix yarn.lock

* Clear cache

* Add test

* Fix yarn.lock

* Fix 204 handling

* Fix yarn.lock

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-04-25 12:47:17 -04:00
Morgan ebbb1dcce3 fix: oAuthClient Creation Form (#14750) 2024-04-25 13:51:03 +00:00
Morgan 8fbb1ed426 chore: bump atoms 1.0.35 and wrap context (#14744) 2024-04-25 11:47:30 +03:00
Omar López d8adc8eeb1 fix: Check permissions for team OOO entries (#14736) 2024-04-24 19:24:59 +00:00
Lauris Skraucis a7bd4aee85 fix: booker atom month reset (#14732) 2024-04-24 14:36:29 +00:00
Lauris SkraucisandMorgan 82fcda4b42 fix: booker atom reschedule data (#14714)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-04-24 13:54:05 +03:00
Lauris SkraucisandMorgan b3b7a09cfb fix: booker atom not resetting month query param (#14715)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-04-24 07:35:33 +00:00
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
Erik c79d0890c0 v3.9.8
release.yml / release (push) Canceled after 0s
2024-04-11 17:11:42 -03:00
Erik 5071e6566f Revert "feat: new App install flow (#11975)" (#14540) 2024-04-11 17:06:17 -03:00
Keith Williams ac77fe360d fix: Invalid 403 forbidden for event-types GET (#14528) 2024-04-11 19:15:21 +00:00
Erik 711aceba0e Revert "Fix: Redirect only those apps that extend 'EventType' to the new app …" (#14539)
This reverts commit 0c67f4940a.
2024-04-11 19:09:13 +00:00
Somay ChauhanandPeer Richelsen f010279303 feat: always show intercom on desktop by default (hidden on mobile) (#14423)
* feat: always show intercom on desktop by default (hidden on mobile)

* Revert "feat: always show intercom on desktop by default (hidden on mobile)"

This reverts commit d14a7b0808a5c3b7f15de75d3a422c4ec5e14f2b.

* feat: always show intercom on desktop by default (hidden on mobile)

* fix: type error

* refactor

* saved showIntercom in localstorage instead of DB

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-04-11 18:58:37 +00:00
Somay Chauhan 0c67f4940a Fix: Redirect only those apps that extend 'EventType' to the new app install flow (#14527) 2024-04-11 18:37:01 +00:00
7836aeacc6 feat: add redirect uris for booking success, cancel and reschedule for a platform user (#14518)
* update typings

* update prisma to include redirect uris for booking success, cancel and reschedule

* update migrations

* add input for uris and send them back in custom hook

* display new uris

* add type for enabling emails

* update prisma schema and add migrations

* chores

* fixup

* chore: fix style, remove console log, rewrite migration

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-04-11 17:46:57 +00:00
Hariom Balhara af0cc5c899 fix: test (#14535)
* Consider requestedSlug as well for instances that require payment

* fix

* Though we should add isTeamEvent to avoid duplicate /event request. But right now that is not breaking functionality
2024-04-11 20:24:16 +03:00
Hariom Balhara aa2808d621 fix: Broken SSR for event booking pages (#14497)
* fix: Broken SSR for event type pages

* Add test

* Consider requestedSlug as well for instances that require payment

* Add a todo test
2024-04-11 10:57:15 +01:00
Morgan 621067acd8 fix: platform gcal connect reconnect credentials (#14513)
* fix: enable gcal connect to reconnect revoked google credentials

* feat: callback to react when check fails
2024-04-11 09:36:35 +00:00
Morgan 3fcba33b1b fix: platform booking endpoints should be public (#14509) 2024-04-11 08:52:31 +00:00
Hariom Balhara 9a66dae0ba feat: In Embed, Fire 'routed' event when Routing Form routes (#14470) 2024-04-11 03:39:28 +00:00
e2ef8d2dc8 fix: correct language is used when booking with a modal + location icon visible in booker when using light mode (#14280)
* fix small frontend issues with Booker

This commit fixes the following:
- The "location" icon is now visible in light move when using the "in person (Organizer Address)" option

- The language in which the date and length is shown in modals now corresponds to the language of the creator of the calendar (before this is was always in english)

- If the text is too large for the modal, they will adapt into a collumn

-

* correct small mistakes

* chore: getDurationFormatted

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-04-10 21:45:55 +00:00
Crowdin Bot eb089bb207 New Crowdin translations by Github Action 2024-04-10 21:42:51 +00:00
Anwar Sadathandzomars 4565c3311c feat: Implement the ability to login via multiple means when signed up via google/non-cal providers (#14082)
Co-authored-by: zomars <zomars@me.com>
2024-04-10 14:38:39 -07:00
John ForteandRajiv Sahal e30b41a85a fix(platform): missing string literal opening (#14463)
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2024-04-10 17:26:08 -04:00
Carina WollendorferandCarinaWolli 7b0321043d fix: OOO for team events (#14503)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-04-10 13:47:26 -07:00
Morgan c4b7062d65 chore: bump atoms to 1.0.13 (#14506) 2024-04-10 20:44:14 +00:00
Meeran E Mandhini eb1c1b9475 Fix locale title typo (#14501) 2024-04-10 17:04:56 -03:00
Erik 67725f0cba feat: SDK managed users + PLA fixes (#14495)
* feat: SDK managed users + PLA fixes

* chore: include lock
2024-04-10 19:53:44 +00:00
Amit Sharma 68c4ad2bf9 fix: private link for a team event gives 404 (#14473)
## What does this PR do?

Fixes #14456 

https://github.com/calcom/cal.com/blob/65df5ac0e4086422bc0b8416f16ba991a10e785d/apps/web/lib/d/%5Blink%5D/%5Bslug%5D/getServerSideProps.tsx#L52-L60

For team events there is no username, so it was returning 404 irrespective of whether anyone was assigned or not.

https://github.com/calcom/cal.com/assets/74371312/b4c8c592-4132-4040-acb2-db37f8f152d6




## Type of change

<!-- Please delete bullets that are not relevant. -->

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


## Mandatory Tasks

- [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
2024-04-10 12:29:45 -07:00
sean-brydon dbafda9822 fix: org-redirect-url-admin-update (#14493) 2024-04-10 11:48:22 -07:00
6d5d2a1c4d feat: new App install flow (#11975)
* feat(appStore): add isOAuth config

* refactor(appStore): EventTypeAppSettngsInterface

* refactor(qr_code): split EventTypeAppCardInterface and EventTypeAppSettingsInterface

* feat(appStore): redirect to onboarding for stripe and basecam

* fix(ui/ScrollableArea): overflow indicator not working

* feat(apps): new install app flow

* fix(configureStep): get disabled props for settings

* fix: getAppInstallsBySlug now use teamIds

* chore: Alby to AppSettingsInterface

* chore: Giphy to appsettings interface

* chore: GTM migration

* chore: GT4 - migration

* chore: fathom migration

* chore: paypal

* feat: basecamp migration

* feat: metapixel migration

* feat:plausable

* feat:stripe

* fix merge issues

* fix merge issues in new app install flow steps

* wip: callback to onboarding

* fix: imports

* fix: more imports

* feat: use redirect to onboarding on install flow

* feat: New app install flow 2 (#14077)

* fix: redirect to event-type after oauth flow

* When clicking on install new app button, only redirect to the new app install flow if it is an oAuth app or an app that extends the EventType feature in it's config

* code refactor, skip event-type step

* removed extra consoles

* so that no one can change the eventTypeId in url

* theme support enabled

* only pass eventTypeId instead of whole evetType obj to OmniInstallAppButton

* fix: clicking on event-type gives error and redirects to /apps

* saved appOnboardingRedirectUrl to OAuth State

* fix: a user having no team breaks the onboarding flow

* add optional chaining to appMetadata.extendsFeature and appMetadata.isOAuth;

* encode appOnboardingRedirectUrl

* fix: dark theme ui issues

* skip configure step if no-event types

if user/team does not have event-type then skip configure step and goto apps/installed/[category]

* exclude calendars from apps- onboarding flow

* Update getAppOnboardingRedirectUrl.ts

* fix: On configure step, inputs aren't showing errors

* change route from `apps/onboarding` to `apps/installation`

* fix: flow not correctly working for teams

* should only allow team ADMIN or OWER to install an app to team

* Prevent two payment apps to be enabled

* removed loading state

* Fix type error

* undo new-app-flow from OmniInstallAppButton

* remove OAuth Step, handle step numbering

* fix: redirect url

* getAppOnboardingRedirectUrl refactor

* for debugging

* Revert "for debugging"

This reverts commit b9362c1a8f47f2280e684c213d7ec08d04a8ac4c.

* fix: added toaster

* feat: added support to select multiple events (almost)

* hide button

* Move inner form outside outer form using portal

Due to limitations with nested forms in React Hook Form, moved the inner form outside the outer form using a portal to ensure proper form handling.

* submit buttons refs madness

* removed configure step

we had to use query params to manage selected event-types, between routes which was causing 2 issues:
* each time we selected or unselected an event the whole page was reloaded including getserversideprops being called again and again
* the form was reloaded causing more issues

* removed extra unwanted code

* small fix

* fix: type error

* fix: types

* implemented suggested changes

* removed step footer
* fixed error.message
* removed commented styles

* removed step footer

* fix: Display team event types instead of user's on event types step when team selected

* fix: typo

* removed shouldLockDisableProps

* refactor setUpdatedEventTypesStatus, fix type

* removed extra code

* can't delete while saving

* added translations

* fix: loading in select account step, fix handle onSelect params type

* Fix typo

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>

* fix: type errors

* fix: failing tests

---------

Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-04-10 16:24:31 +00:00
Morgan b748b8d077 chore: AtomsWrapper to scope css (#14490) 2024-04-10 16:21:20 +00:00
f86c448359 fix: view team bookings (#14079)
* fix: view team bookings

* fix: edge case- common user across 2 different teams

* @SomayChauhan fix: edge case- common user across 2 different teams v2.0

* feat: added booking list test for collective event-types

* feat: added booking list test for round-robin event-types

* feat: added test for managed event types bookings

* feat: added test for individual events bookings

* refactor code

* added comments

* added isFixed to collective event hosts

* better function naming

* code refactor

* fix: assertBookingVisibleFor ignoring today's meetings

* fix: handling case for organizations

* fix: non org bookings-list test

* feat: added tests for organisations booking list

* added comments for better clarity

* booking-list in org tests

* uncommented doOnOrgDomain

* removed extra selectors

* fix: failing tests

* fix: db-load on bookings.get

* fix: only select the required fields

* followUp: Improve getUniqueBookings perf

---------

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: Omar López <zomars@me.com>
2024-04-10 16:57:04 +01:00
Vachmara d0af4b8ddd fix: error when adding through URL (#14496) 2024-04-10 14:32:32 +00:00
Morgan 39103c07c9 fix: unnecessary access token guard on public endpoint for slots (#14492) 2024-04-10 12:31:26 +00:00
Lauris Skraucis 8382ebc057 refactor: v2 docs (#14446)
* refactor: group endpoints by tags

* refactor: remove swagger header

* feat: managed users docs

* feat: gcal docs

* feat: provider docs

* feat: managed users docs

* fix: event-types locations docs

* refactor: schedules remove forAtom logic - return just for atoms

* feat: schedules docs

* feat: schedules docs

* fix: me tests

* fix: schedules tests

* feat: me docs

* feat: calendar busy times docs

* feat: calendar connected and destination calendars docs

* feat: some docs for bookings endpoints

* fix: add OAuth id when updating user email + disallow updating username

* fix: creating user with same e-mail results in 500

* fix: me e2e tests

* fix: event-types e2e tests

* fix: oauth-client-users e2e tests

* fix: some bookings e2e

* revert: re-add name for create update user

* fix: schedule create isDefault required

* lock file from main

* lock file from main

* comment broken bookings tests

* fix: connected calendars output type

* fix: connected calendars output type
2024-04-10 12:08:41 +03:00
880ba8f1dd feat: ooo-v2 (#13621)
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-04-09 18:29:39 -07:00
Alex van Andel f00849b8d1 fix: Remove logo from getPublicEvent response (#14488) 2024-04-09 23:23:54 +00:00
Alex van Andel c090f78898 fix: isTeamEvent would eval to null when undefined (#14480) 2024-04-09 21:01:39 +00:00
Hariom Balhara 46a1713122 fix: Remove unnecessary export in barrel file which is causing prisma import on client side (#14475) 2024-04-09 15:37:18 -03:00
4a88b33782 refactor: Add crm type to app credentials (#14147)
* 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

* Fix tests

* Type fix

---------

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>
2024-04-09 16:46:06 +00:00
Udit Takkar a1a2a141e4 chore: add instruction in recording email (#14424) 2024-04-09 09:32:10 -04:00
Crowdin Bot 97404455d4 New Crowdin translations by Github Action 2024-04-09 13:10:36 +00:00
Alex van Andel be31100bd7 fix: Ensure the session is invalidated during booking-seats test (#14478) 2024-04-09 13:05:40 +00:00
Keith Williams 4335c9ab92 v3.9.7
release.yml / release (push) Canceled after 0s
2024-04-09 09:37:32 -03:00
Peer Richelsen 20df7156cf Update common.json (#14472) 2024-04-09 12:23:54 +00:00
ErikandPeer Richelsen 9c35425e57 chore: Release SDK 1.0.0 (#14462)
* chore: dont require version

* fixup: type

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-04-09 11:56:02 +00:00
Hariom Balhara 65df5ac0e4 fix: Allow unpublished org to serve redirected links plus other org onboarding fixes (#14337)
* fix: Allow unpublished org to serve redirected links

* fix: Fix team event link for unpublished org

* fixes

* Rename variable

* Add minimum teams requirement for upgrade

* Fixes

* PR Feedback addressed
2024-04-09 10:33:39 +01:00
Keith Williams 84fdd03603 fix: Return team event type (#14464) 2024-04-08 18:35:41 -03:00
Crowdin Bot e6ec8bdf4d New Crowdin translations by Github Action 2024-04-08 19:17:45 +00:00
Yamini KandKeith Williams 86c8197104 docs: Update README.md regarding POSTGRES db port (#14431)
* Update README.md

Updating the description about POSTGRES DB port

* Update README.md

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-04-08 19:12:11 +00:00
Pat bb13badcf4 Update README.md (#14432)
Add minimum version number to PostgreSQL in 'Getting Started,' to help potential contributors successfully set up environment with local database.

I tried to set up local dev environment on Ubuntu 20.04 with PostgreSQL 12.18 which failed when running 'yarn workspace @calcom/prisma db-migrate'

PostgreSQL version 14.11 worked for me. I saw another dev say v. 13+ would work. Could save folks some troubleshooting time.

This is the error on PostgreSQL 12.18:

Error: P3006 Migration `20240321143215_move_avatars_cols_to_avatar_table` failed to apply cleanly to the shadow database. Error: ERROR: function gen_random_uuid() does not exist HINT: No function matches the given name and argument types. You might need to add explicit type casts. 0: sql_schema_connector::validate_migrations with namespaces=None at schema-engine/connectors/sql-schema-connector/src/lib.rs:324 1: schema_core::state::DevDiagnostic at schema-engine/core/src/state.rs:270
2024-04-08 16:09:43 -03:00
890 changed files with 26525 additions and 6985 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
+6 -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
@@ -299,6 +299,7 @@ E2E_TEST_CALCOM_GCAL_KEYS=
CALCOM_CREDENTIAL_SYNC_SECRET=""
# This is the header name that will be used to verify the webhook secret. Should be in lowercase
CALCOM_CREDENTIAL_SYNC_HEADER_NAME="calcom-credential-sync-secret"
# This the endpoint from which the token is fetched
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
# Key should match on Cal.com and your application
# must be 24 bytes for AES256 encryption algorithm
@@ -347,6 +348,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=
+6 -24
View File
@@ -2,29 +2,18 @@
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes # (issue)
- Fixes #XXXX (GitHub issue number)
- Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description)
<!-- Please provide a loom video for visual changes to speed up reviews
Loom Video: https://www.loom.com/
-->
## Requirement/Documentation
## Mandatory Tasks (DO NOT REMOVE)
<!-- Please provide all documents that are important to understand the reason of that PR. -->
- If there is a requirement document, please, share it here.
- If there is a UI/UX design document, please, share it here.
## Type of change
<!-- Please delete bullets that are not relevant. -->
- Bug fix (non-breaking change which fixes an issue)
- Chore (refactoring code, technical debt, workflow improvements)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
- Tests (Unit/Integration/E2E or any other test)
- This change requires a documentation update
- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected)
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com)
- [ ] I have added or modified automated tests that prove my fix is effective or that my feature works (PRs might be rejected if logical changes are not properly tested)
## How should this be tested?
@@ -35,10 +24,6 @@ Fixes # (issue)
- What is expected (happy path) to have (input and output)?
- Any other important info that could help to test that PR
## Mandatory Tasks
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
## Checklist
<!-- Remove bullet points below that don't apply to you -->
@@ -46,7 +31,4 @@ Fixes # (issue)
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my PR needs changes to the documentation
- I haven't checked if my changes generate no new warnings
- I haven't added tests that prove my fix is effective or that my feature works
- I haven't checked if new and existing unit tests pass locally with my changes
+4 -1
View File
@@ -11,11 +11,14 @@ runs:
uses: buildjet/cache@v3
id: cache-build
env:
# WARN: Don't touch this cache key. Currently github.sha refers to the latest commit in main
# and not the branch that's attempting to merge to main, which causes CI errors when merged
# to main.
# TODO: Fix this problem if intending to modify this cache key.
cache-name: prod-build
key-1: ${{ inputs.node_version }}-${{ hashFiles('yarn.lock') }}
key-2: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', '!**/node_modules') }}
key-3: ${{ github.event.pull_request.number || github.ref }}
# Ensures production-build.yml will always be fresh
key-4: ${{ github.sha }}
with:
path: |
@@ -1,4 +1,4 @@
name: Production Build
name: Production Builds
on:
workflow_call:
@@ -18,7 +18,10 @@ env:
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
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_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
@@ -33,12 +36,10 @@ env:
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
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 }}
jobs:
build:
name: Build API V1
name: Build API v1
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 30
services:
@@ -0,0 +1,42 @@
name: Production Build
on:
workflow_call:
env:
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
jobs:
build:
name: Build API v2
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 30
services:
postgres:
image: postgres:13
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: calendso
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- run: |
export NODE_OPTIONS="--max_old_space_size=8192"
yarn workspace @calcom/api-v2 run generate-schemas
rm -rf apps/api/v2/node_modules
yarn install
yarn workspace @calcom/api-v2 run build
shell: bash
+2 -2
View File
@@ -6,7 +6,7 @@ on:
jobs:
cleanup:
runs-on: buildjet-4vcpu-ubuntu-2204
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
- name: Check out code
uses: actions/checkout@v3
@@ -21,7 +21,7 @@ jobs:
echo "Fetching list of cache key"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
## Setting this to not fail the workflow while deleting cache keys.
## Setting this to not fail the workflow while deleting cache keys.
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR
+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
+35 -32
View File
@@ -3,6 +3,41 @@ on:
workflow_call:
env:
NODE_OPTIONS: --max-old-space-size=4096
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 }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
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_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
e2e-app-store:
timeout-minutes: 20
@@ -59,38 +94,6 @@ jobs:
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
+35 -27
View File
@@ -3,6 +3,41 @@ on:
workflow_call:
env:
NODE_OPTIONS: --max-old-space-size=4096
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 }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
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_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
e2e-embed:
timeout-minutes: 20
@@ -46,33 +81,6 @@ jobs:
run: |
yarn e2e:embed-react --shard=${{ matrix.shard }}/${{ strategy.job-total }}
yarn workspace @calcom/embed-react packaged:tests
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
+35 -28
View File
@@ -3,6 +3,41 @@ on:
workflow_call:
env:
NODE_OPTIONS: --max-old-space-size=4096
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 }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
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_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
e2e-embed:
timeout-minutes: 20
@@ -52,34 +87,6 @@ jobs:
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn e2e:embed --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
+37 -34
View File
@@ -3,11 +3,46 @@ on:
workflow_call:
env:
NODE_OPTIONS: --max-old-space-size=4096
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 }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
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_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
e2e:
timeout-minutes: 20
name: E2E tests (${{ matrix.shard }}/${{ strategy.job-total }})
runs-on: buildjet-4vcpu-ubuntu-2204
runs-on: buildjet-8vcpu-ubuntu-2204
services:
postgres:
image: postgres:13
@@ -36,7 +71,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
shard: [1, 2, 3, 4, 5, 6, 7]
steps:
- uses: docker/login-action@v3
with:
@@ -50,38 +85,6 @@ jobs:
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
+40 -3
View File
@@ -7,16 +7,53 @@ on:
branches:
- main
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 }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
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_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
build:
name: Production build
name: Production builds
if: ${{ github.event_name == 'push' }}
uses: ./.github/workflows/production-build.yml
uses: ./.github/workflows/production-build-without-database.yml
secrets: inherit
analyze:
needs: build
if: always()
runs-on: buildjet-4vcpu-ubuntu-2204
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
+17 -17
View File
@@ -14,7 +14,7 @@ concurrency:
jobs:
changes:
name: Detect changes
runs-on: buildjet-4vcpu-ubuntu-2204
runs-on: buildjet-2vcpu-ubuntu-2204
permissions:
pull-requests: read
outputs:
@@ -49,22 +49,22 @@ jobs:
uses: ./.github/workflows/lint.yml
secrets: inherit
build:
name: Production build
needs: [changes]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/production-build.yml
secrets: inherit
build-api-v1:
name: Production build
name: Production builds
needs: [changes]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/api-v1-production-build.yml
secrets: inherit
build-without-database:
name: Production build (without database)
build-api-v2:
name: Production builds
needs: [changes]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/api-v2-production-build.yml
secrets: inherit
build:
name: Production builds
needs: [changes]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/production-build-without-database.yml
@@ -72,28 +72,28 @@ jobs:
e2e:
name: E2E tests
needs: [changes, lint, build]
needs: [changes, lint, build, build-api-v1, build-api-v2]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e.yml
secrets: inherit
e2e-app-store:
name: E2E App Store tests
needs: [changes, lint, build]
needs: [changes, lint, build, build-api-v1, build-api-v2]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-app-store.yml
secrets: inherit
e2e-embed:
name: E2E embeds tests
needs: [changes, lint, build]
needs: [changes, lint, build, build-api-v1, build-api-v2]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-embed.yml
secrets: inherit
e2e-embed-react:
name: E2E React embeds tests
needs: [changes, lint, build]
needs: [changes, lint, build, build-api-v1, build-api-v2]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-embed-react.yml
secrets: inherit
@@ -106,9 +106,9 @@ jobs:
secrets: inherit
required:
needs: [changes, lint, type-check, test, build, build-api-v1, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
needs: [changes, lint, type-check, test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
if: always()
runs-on: buildjet-4vcpu-ubuntu-2204
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
- name: fail if conditional jobs failed
if: needs.changes.outputs.has-files-requiring-all-checks == 'true' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled'))
+4 -9
View File
@@ -39,8 +39,8 @@ jobs:
secrets: inherit
build:
name: Production build
uses: ./.github/workflows/production-build.yml
name: Production builds
uses: ./.github/workflows/production-build-without-database.yml
secrets: inherit
e2e:
@@ -67,15 +67,10 @@ jobs:
uses: ./.github/workflows/e2e-embed-react.yml
secrets: inherit
build-without-database:
name: Production build (without database)
uses: ./.github/workflows/production-build-without-database.yml
secrets: inherit
required:
needs: [e2e, e2e-app-store, e2e-embed, e2e-embed-react, build-without-database]
needs: [changes, lint, build, e2e, e2e-app-store, e2e-embed, e2e-embed-react]
if: always()
runs-on: buildjet-4vcpu-ubuntu-2204
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
- name: fail if conditional jobs failed
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
@@ -1,11 +1,11 @@
name: Production Build (without database)
name: Production Builds
on:
workflow_call:
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,12 +31,13 @@ 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:
name: Build
runs-on: ubuntu-latest
name: Web App
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
-67
View File
@@ -1,67 +0,0 @@
name: Production Build
on:
workflow_call:
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_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 }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
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 }}
jobs:
build:
name: Build
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 30
services:
postgres:
image: postgres:13
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: calendso
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
+1 -1
View File
@@ -7,7 +7,7 @@ on:
jobs:
test:
timeout-minutes: 20
runs-on: buildjet-4vcpu-ubuntu-2204
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
+4 -4
View File
@@ -24,7 +24,7 @@
<p align="center">
<a href="https://go.cal.com/discord"><img src="https://img.shields.io/badge/Discord-go.cal.com%2Fdiscord-%234A154B" alt="Join Cal.com Discord"></a>
<a href="https://producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
<a href="https://www.producthunt.com/products/cal-com"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
<a href="https://status.cal.com"><img height="20px" src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
<a href="https://news.ycombinator.com/item?id=34507672"><img src="https://img.shields.io/badge/Hacker%20News-%231-%23FF6600" alt="Hacker News"></a>
@@ -33,7 +33,7 @@
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-Free-brightgreen" alt="Pricing"></a>
<a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a>
<img src="https://api.checklyhq.com/v1/badges/checks/5e048048-1b51-47ba-9209-60607507622e?responseTime=true" alt="Checkly Availability" />
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
<a href="https://hub.docker.com/r/calcom/cal.com"><img src="https://img.shields.io/docker/pulls/calcom/cal.com"></a>
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
@@ -113,7 +113,7 @@ To get a local copy up and running, please follow these simple steps.
Here is what you need to be able to run Cal.com.
- Node.js (Version: >=18.x)
- PostgreSQL
- PostgreSQL (Version: >=13.x)
- Yarn _(recommended)_
> If you want to enable any of the available integrations, you may want to obtain additional credentials for each one. More details on this can be found below under the [integrations section](#integrations).
@@ -210,7 +210,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
![image](https://user-images.githubusercontent.com/39329182/236612291-51d87f69-6dc1-4a23-bf4d-1ca1754e0a35.png)
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`.
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`. The port is configurable and does not have to be 5432.
</details>
@@ -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",
+1
View File
@@ -82,4 +82,5 @@ export const schemaBookingReadPublic = Booking.extend({
metadata: true,
status: true,
responses: true,
fromReschedule: true,
});
+3
View File
@@ -6,6 +6,9 @@ export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true })
hideBranding: true,
metadata: true,
pendingPayment: true,
isOrganization: true,
isPlatform: true,
smsLockState: true,
});
const schemaTeamRequiredParams = z.object({
-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,6 +1,6 @@
import type { NextApiRequest } from "next";
import { minimumTokenResponseSchema } from "@calcom/app-store/_utils/oauth/parseRefreshTokenResponse";
import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
@@ -63,7 +63,7 @@ async function handler(req: NextApiRequest) {
symmetricDecrypt(encryptedKey, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
);
const key = minimumTokenResponseSchema.parse(decryptedKey);
const key = OAuth2UniversalSchema.parse(decryptedKey);
const credential = await prisma.credential.update({
where: {
@@ -1,7 +1,7 @@
import type { NextApiRequest } from "next";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { minimumTokenResponseSchema } from "@calcom/app-store/_utils/oauth/parseRefreshTokenResponse";
import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { HttpError } from "@calcom/lib/http-error";
@@ -70,7 +70,7 @@ async function handler(req: NextApiRequest) {
symmetricDecrypt(encryptedKey, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
);
const key = minimumTokenResponseSchema.parse(decryptedKey);
const key = OAuth2UniversalSchema.parse(decryptedKey);
// Need to get app type
const app = await prisma.app.findUnique({
+5
View File
@@ -57,6 +57,11 @@ const swaggerHandler = withSwagger({
type: "string",
example: "Europe/London",
},
fromReschedule: {
type: "string",
nullable: true,
format: "uuid",
},
attendees: {
type: "array",
items: {
@@ -1,18 +0,0 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
async function authMiddleware(req: NextApiRequest) {
const { userId, isAdmin } = req;
const { id } = schemaQueryIdParseInt.parse(req.query);
if (isAdmin) return;
const eventType = await prisma.eventType.findFirst({
where: { id, users: { some: { id: userId } } },
});
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
}
export default authMiddleware;
@@ -93,6 +93,7 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
await canAccessTeamEventOrThrow(req, {
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
});
return true;
}
if (eventType?.userId === req.userId) return true; // is owner.
throw new HttpError({ statusCode: 403, message: "Forbidden" });
@@ -4,11 +4,8 @@ import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import { withMiddleware } from "~/lib/helpers/withMiddleware";
import authMiddleware from "./_auth-middleware";
export default withMiddleware()(
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
await authMiddleware(req);
return defaultHandler({
GET: import("./_get"),
PATCH: import("./_patch"),
@@ -26,7 +26,7 @@ export default async function checkTeamEventEditPermission(
if (!membership?.role || !["ADMIN", "OWNER"].includes(membership.role)) {
throw new HttpError({
statusCode: 401,
statusCode: 403,
message: "No permission to operate on event-type for this team",
});
}
+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";
@@ -0,0 +1,143 @@
import prismaMock from "../../../../../../../tests/libs/__mocks__/prismaMock";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test } from "vitest";
import { buildEventType } from "@calcom/lib/test/builder";
import { MembershipRole } from "@calcom/prisma/enums";
import handler from "../../../../pages/api/event-types/[id]/_get";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
describe("GET /api/event-types/[id]", () => {
describe("Errors", () => {
test("Returns 403 if user not admin/team member/event owner", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: 123456,
},
});
prismaMock.eventType.findUnique.mockResolvedValue(
buildEventType({
id: 123456,
userId: 444444,
})
);
req.userId = 333333;
await handler(req, res);
expect(res.statusCode).toBe(403);
});
});
describe("Success", async () => {
test("Returns event type if user is admin", async () => {
const eventTypeId = 123456;
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: eventTypeId,
},
});
prismaMock.eventType.findUnique.mockResolvedValue(
buildEventType({
id: eventTypeId,
})
);
req.isAdmin = true;
req.userId = 333333;
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
});
test("Returns event type if user is in team associated with event type", async () => {
const eventTypeId = 123456;
const teamId = 9999;
const userId = 333333;
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: eventTypeId,
},
});
prismaMock.eventType.findUnique.mockResolvedValue(
buildEventType({
id: eventTypeId,
teamId,
})
);
prismaMock.team.findFirst.mockResolvedValue({
id: teamId,
members: [
{
userId,
},
],
});
req.isAdmin = false;
req.userId = userId;
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
expect(prismaMock.team.findFirst).toHaveBeenCalledWith({
where: {
id: teamId,
members: {
some: {
userId: req.userId,
role: {
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
},
},
},
},
});
});
test("Returns event type if user is the event type owner", async () => {
const eventTypeId = 123456;
const userId = 333333;
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: eventTypeId,
},
});
prismaMock.eventType.findUnique.mockResolvedValue(
buildEventType({
id: eventTypeId,
userId,
scheduleId: 1111,
})
);
req.isAdmin = false;
req.userId = userId;
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
expect(prismaMock.team.findFirst).not.toHaveBeenCalled();
});
});
});
+10 -1
View File
@@ -10,4 +10,13 @@ JWT_SECRET=
SENTRY_DSN=
# KEEP THIS EMPTY, DISABLE SENTRY CLIENT INSIDE OF LIBRARIES USED BY APIv2
NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_SENTRY_DSN=
# Stripe Billing
STRIPE_PRICE_ID_STARTER=
STRIPE_PRICE_ID_ESSENTIALS=
STRIPE_PRICE_ID_ENTERPRISE=
STRIPE_API_KEY=
STRIPE_WEBHOOK_SECRET=
WEB_APP_URL=http://localhost:3000/
+1 -1
View File
@@ -12,7 +12,7 @@ module.exports = {
node: true,
jest: true,
},
ignorePatterns: [".eslintrc.js"],
ignorePatterns: [".eslintrc.js", "next-i18next.config.js"],
rules: {
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
+1
View File
@@ -2,6 +2,7 @@
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"entryFile": "./apps/api/v2/src/main.js",
"compilerOptions": {
"deleteOutDir": true,
"plugins": [
+5 -3
View File
@@ -11,9 +11,9 @@
"start": "nest start",
"dev:build:watch": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch & yarn workspace @calcom/platform-libraries build:watch",
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build",
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && nest start --watch",
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && yarn start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/src/main",
"start:prod": "node ./dist/apps/api/v2/src/main.js",
"test": "yarn dev:build && jest",
"test:watch": "yarn dev:build && jest --watch",
"test:cov": "yarn dev:build && jest --coverage",
@@ -37,9 +37,10 @@
"@nestjs/passport": "^10.0.2",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.3.0",
"@nestjs/throttler": "^5.1.1",
"@nestjs/throttler": "^5.1.2",
"@sentry/node": "^7.86.0",
"@sentry/tracing": "^7.86.0",
"body-parser": "^1.20.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
@@ -56,6 +57,7 @@
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"stripe": "^15.3.0",
"uuid": "^8.3.2",
"winston": "^3.11.0",
"zod": "^3.22.4"
+4
View File
@@ -1,6 +1,10 @@
import { getEnv } from "@/env";
import { Controller, Get, Version, VERSION_NEUTRAL } from "@nestjs/common";
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
@Controller()
@DocsTags("Health - development only")
@DocsExcludeController(getEnv("NODE_ENV") === "production")
export class AppController {
@Get("health")
@Version(VERSION_NEUTRAL)
+54 -17
View File
@@ -1,11 +1,21 @@
import { AppLoggerMiddleware } from "@/app.logger.middleware";
import appConfig from "@/config/app";
import { AppLoggerMiddleware } from "@/middleware/app.logger.middleware";
import { RewriterMiddleware } from "@/middleware/app.rewrites.middleware";
import { JsonBodyMiddleware } from "@/middleware/body/json.body.middleware";
import { RawBodyMiddleware } from "@/middleware/body/raw.body.middleware";
import { ResponseInterceptor } from "@/middleware/request-ids/request-id.interceptor";
import { RequestIdMiddleware } from "@/middleware/request-ids/request-id.middleware";
import { AuthModule } from "@/modules/auth/auth.module";
import { EndpointsModule } from "@/modules/endpoints.module";
import { JwtModule } from "@/modules/jwt/jwt.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { RedisModule } from "@/modules/redis/redis.module";
import { RedisService } from "@/modules/redis/redis.service";
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { APP_INTERCEPTOR, RouterModule } from "@nestjs/core";
import { seconds, ThrottlerModule } from "@nestjs/throttler";
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";
import { AppController } from "./app.controller";
@@ -16,29 +26,56 @@ import { AppController } from "./app.controller";
isGlobal: true,
load: [appConfig],
}),
// ThrottlerModule.forRootAsync({
// imports: [ConfigModule],
// inject: [ConfigService],
// useFactory: (config: ConfigService<AppConfig>) => ({
// throttlers: [
// {
// name: "short",
// ttl: seconds(10),
// limit: 3,
// },
// ],
// storage: new ThrottlerStorageRedisService(config.get("db.redisUrl", { infer: true })),
// }),
// }),
RedisModule,
ThrottlerModule.forRootAsync({
imports: [RedisModule],
inject: [RedisService],
useFactory: (redisService: RedisService) => ({
throttlers: [
{
name: "short",
ttl: seconds(10),
limit: 3,
},
{
name: "medium",
ttl: seconds(30),
limit: 10,
},
],
storage: new ThrottlerStorageRedisService(redisService.redis),
}),
}),
PrismaModule,
EndpointsModule,
AuthModule,
JwtModule,
//register prefix for all routes in EndpointsModule
RouterModule.register([{ path: "/v2", module: EndpointsModule }]),
],
controllers: [AppController],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ResponseInterceptor,
},
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer.apply(AppLoggerMiddleware).forRoutes("*");
consumer
.apply(RawBodyMiddleware)
.forRoutes({
path: "/api/v2/billing/webhook",
method: RequestMethod.POST,
})
.apply(JsonBodyMiddleware)
.forRoutes("*")
.apply(RequestIdMiddleware)
.forRoutes("*")
.apply(AppLoggerMiddleware)
.forRoutes("*")
.apply(RewriterMiddleware)
.forRoutes("/");
}
}
+4 -6
View File
@@ -4,13 +4,15 @@ import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { SentryFilter } from "@/filters/sentry-exception.filter";
import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
import type { ValidationError } from "@nestjs/common";
import { BadRequestException, RequestMethod, ValidationPipe, VersioningType } from "@nestjs/common";
import { BadRequestException, ValidationPipe, VersioningType } from "@nestjs/common";
import { HttpAdapterHost } from "@nestjs/core";
import type { NestExpressApplication } from "@nestjs/platform-express";
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;
+8 -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"),
@@ -24,6 +24,13 @@ const loadConfig = (): AppConfig => {
next: {
authSecret: getEnv("NEXTAUTH_SECRET"),
},
stripe: {
apiKey: getEnv("STRIPE_API_KEY"),
webhookSecret: getEnv("STRIPE_WEBHOOK_SECRET"),
},
app: {
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
},
};
};
+7
View File
@@ -15,4 +15,11 @@ export type AppConfig = {
next: {
authSecret: string;
};
stripe: {
apiKey: string;
webhookSecret: string;
};
app: {
baseUrl: string;
};
};
@@ -1,13 +1,16 @@
import { BookingsController } from "@/ee/bookings/controllers/bookings.controller";
import { BillingModule } from "@/modules/billing/billing.module";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, TokensModule],
providers: [TokensRepository, OAuthFlowService],
imports: [PrismaModule, RedisModule, TokensModule, BillingModule],
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository],
controllers: [BookingsController],
})
export class BookingsModule {}
@@ -1,7 +1,11 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
import { SchedulesModule } from "@/ee/schedules/schedules.module";
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
@@ -11,17 +15,12 @@ import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import * as request from "supertest";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import {
getAllUserBookings,
handleNewBooking,
getBookingInfo,
handleNewRecurringBooking,
handleInstantMeeting,
} from "@calcom/platform-libraries";
import { handleNewBooking } from "@calcom/platform-libraries";
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
describe("Bookings Endpoints", () => {
@@ -30,28 +29,50 @@ describe("Bookings Endpoints", () => {
let userRepositoryFixture: UserRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
const userEmail = "bookings-controller-e2e@api.com";
let user: User;
let eventTypeId: number;
let createdBooking: Awaited<ReturnType<typeof handleNewBooking>>;
beforeAll(async () => {
const moduleRef = await withAccessTokenAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule],
providers: [SchedulesRepository, SchedulesService],
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule, SchedulesModule],
})
).compile();
)
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService>(SchedulesService);
user = await userRepositoryFixture.create({
email: userEmail,
});
const userSchedule: CreateScheduleInput = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
user.id
);
eventTypeId = event.id;
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
@@ -64,13 +85,23 @@ describe("Bookings Endpoints", () => {
});
it("should create a booking", async () => {
const bookingStart = "2023-05-25T09:30:00.000Z";
const bookingEnd = "2023-05-25T10:30:00.000Z";
const bookingEventTypeId = 7;
const bookingTimeZone = "Europe/Londom";
const bookingStart = "2040-05-21T09:30:00.000Z";
const bookingEnd = "2040-05-21T10:30:00.000Z";
const bookingEventTypeId = eventTypeId;
const bookingTimeZone = "Europe/London";
const bookingLanguage = "en";
const bookingHashedLink = "";
const bookingMetadata = {};
const bookingResponses = {
name: "tester",
email: "tester@example.com",
location: {
value: "link",
optionValue: "",
},
notes: "test",
guests: [],
};
const body = {
start: bookingStart,
@@ -80,10 +111,11 @@ describe("Bookings Endpoints", () => {
language: bookingLanguage,
metadata: bookingMetadata,
hashedLink: bookingHashedLink,
responses: bookingResponses,
};
return request(app.getHttpServer())
.post("/api/v2/ee/bookings")
.post("/v2/bookings")
.send(body)
.expect(201)
.then(async (response) => {
@@ -91,8 +123,8 @@ describe("Bookings Endpoints", () => {
response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toBeDefined();
expect(responseBody.data.user.email).toEqual(userEmail);
expect(responseBody.data.userPrimaryEmail).toBeDefined();
expect(responseBody.data.userPrimaryEmail).toEqual(userEmail);
expect(responseBody.data.id).toBeDefined();
expect(responseBody.data.uid).toBeDefined();
expect(responseBody.data.startTime).toEqual(bookingStart);
@@ -106,10 +138,10 @@ describe("Bookings Endpoints", () => {
it("should get bookings", async () => {
return request(app.getHttpServer())
.get("/api/v2/ee/bookings")
.get("/v2/bookings?filters[status]=upcoming")
.then((response) => {
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof getAllUserBookings>>> =
response.body;
console.log("asap responseBody", JSON.stringify(response.body, null, 2));
const responseBody: GetBookingsOutput = response.body;
const fetchedBooking = responseBody.data.bookings[0];
expect(responseBody.data.bookings.length).toEqual(1);
@@ -121,16 +153,16 @@ describe("Bookings Endpoints", () => {
expect(fetchedBooking.uid).toEqual(createdBooking.uid);
expect(fetchedBooking.startTime).toEqual(createdBooking.startTime);
expect(fetchedBooking.endTime).toEqual(createdBooking.endTime);
expect(fetchedBooking.user?.email).toEqual(createdBooking.user.email);
expect(fetchedBooking.user?.email).toEqual(userEmail);
});
});
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: ApiSuccessResponse<Awaited<ReturnType<typeof getBookingInfo>>> = response.body;
const { bookingInfo } = responseBody.data;
const responseBody: GetBookingOutput = response.body;
const bookingInfo = responseBody.data;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
@@ -143,68 +175,71 @@ describe("Bookings Endpoints", () => {
});
});
it("should create a recurring booking", async () => {
const bookingStart = "2023-05-25T09:30:00.000Z";
const bookingEnd = "2023-05-25T10:30:00.000Z";
const bookingEventTypeId = 7;
const bookingTimeZone = "Europe/Londom";
const bookingLanguage = "en";
const bookingHashedLink = "";
const bookingRecurringCount = 5;
const currentBookingRecurringIndex = 0;
// note(Lauris) : found this test broken here - first thing to fix is that recurring endpoint accepts an array not 1 object.
// it("should create a recurring booking", async () => {
// const bookingStart = "2040-05-25T09:30:00.000Z";
// const bookingEnd = "2040-05-25T10:30:00.000Z";
// const bookingEventTypeId = 7;
// const bookingTimeZone = "Europe/London";
// const bookingLanguage = "en";
// const bookingHashedLink = "";
// const bookingRecurringCount = 5;
// const currentBookingRecurringIndex = 0;
const body = {
start: bookingStart,
end: bookingEnd,
eventTypeId: bookingEventTypeId,
timeZone: bookingTimeZone,
language: bookingLanguage,
metadata: {},
hashedLink: bookingHashedLink,
recurringCount: bookingRecurringCount,
currentRecurringIndex: currentBookingRecurringIndex,
};
// const body = {
// start: bookingStart,
// end: bookingEnd,
// eventTypeId: bookingEventTypeId,
// timeZone: bookingTimeZone,
// language: bookingLanguage,
// metadata: {},
// hashedLink: bookingHashedLink,
// recurringCount: bookingRecurringCount,
// currentRecurringIndex: currentBookingRecurringIndex,
// };
return request(app.getHttpServer())
.post("/api/v2/ee/bookings/reccuring")
.send(body)
.expect(201)
.then((response) => {
const responseBody: ApiResponse<Awaited<ReturnType<typeof handleNewRecurringBooking>>> =
response.body;
// return request(app.getHttpServer())
// .post("/v2/bookings/recurring")
// .send(body)
// .expect(201)
// .then((response) => {
// const responseBody: ApiResponse<Awaited<ReturnType<typeof handleNewRecurringBooking>>> =
// response.body;
expect(responseBody.status).toEqual("recurring");
});
});
// expect(responseBody.status).toEqual("recurring");
// });
// });
it("should create an instant booking", async () => {
const bookingStart = "2023-05-25T09:30:00.000Z";
const bookingEnd = "2023-05-25T10:30:00.000Z";
const bookingEventTypeId = 7;
const bookingTimeZone = "Europe/Londom";
const bookingLanguage = "en";
const bookingHashedLink = "";
// note(Lauris) : found this test broken here - first thing to fix is that the eventTypeId must be team event type, because
// instant bookings only work for teams.
// it("should create an instant booking", async () => {
// const bookingStart = "2040-05-25T09:30:00.000Z";
// const bookingEnd = "2040-25T10:30:00.000Z";
// const bookingEventTypeId = 7;
// const bookingTimeZone = "Europe/London";
// const bookingLanguage = "en";
// const bookingHashedLink = "";
const body = {
start: bookingStart,
end: bookingEnd,
eventTypeId: bookingEventTypeId,
timeZone: bookingTimeZone,
language: bookingLanguage,
metadata: {},
hashedLink: bookingHashedLink,
};
// const body = {
// start: bookingStart,
// end: bookingEnd,
// eventTypeId: bookingEventTypeId,
// timeZone: bookingTimeZone,
// language: bookingLanguage,
// metadata: {},
// hashedLink: bookingHashedLink,
// };
return request(app.getHttpServer())
.post("/api/v2/ee/bookings/instant")
.send(body)
.expect(201)
.then((response) => {
const responseBody: ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>> = response.body;
// return request(app.getHttpServer())
// .post("/v2/bookings/instant")
// .send(body)
// .expect(201)
// .then((response) => {
// const responseBody: ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>> = response.body;
expect(responseBody.status).toEqual("instant");
});
});
// expect(responseBody.status).toEqual("instant");
// });
// });
it("should cancel a booking", async () => {
const bookingId = createdBooking.id;
@@ -215,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,9 +1,13 @@
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 { BillingService } from "@/modules/billing/services/billing.service";
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 {
@@ -13,6 +17,7 @@ import {
Req,
InternalServerErrorException,
Body,
Headers,
HttpException,
Param,
Get,
@@ -20,11 +25,13 @@ import {
NotFoundException,
UseGuards,
} from "@nestjs/common";
import { ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { Request } from "express";
import { NextApiRequest } from "next/types";
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
import { BOOKING_READ, SUCCESS_STATUS } from "@calcom/platform-constants";
import {
getAllUserBookings,
getBookingInfo,
@@ -38,30 +45,58 @@ import {
handleNewRecurringBooking,
handleInstantMeeting,
} from "@calcom/platform-libraries";
import { GetBookingsInput, CancelBookingInput } from "@calcom/platform-types";
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
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)
@DocsTags("Bookings")
export class BookingsController {
private readonly logger = new Logger("ee bookings controller");
private readonly logger = new Logger("BookingsController");
constructor(
private readonly oAuthFlowService: OAuthFlowService,
private readonly prismaReadService: PrismaReadService
private readonly prismaReadService: PrismaReadService,
private readonly oAuthClientRepository: OAuthClientRepository,
private readonly billingService: BillingService
) {}
// note(Rajiv): currently this endpoint is atoms only
@Get("/")
@UseGuards(AccessTokenGuard)
@Permissions([BOOKING_READ])
@ApiQuery({ name: "filters[status]", enum: Status, required: true })
@ApiQuery({ name: "limit", type: "number", required: false })
@ApiQuery({ name: "cursor", type: "number", required: false })
async getBookings(
@GetUser() user: User,
@Query() queryParams: GetBookingsInput
): Promise<ApiResponse<unknown>> {
): Promise<GetBookingsOutput> {
const { filters, cursor, limit } = queryParams;
const bookings = await getAllUserBookings({
bookingListingByStatus: filters.status,
@@ -80,9 +115,8 @@ export class BookingsController {
};
}
// note(Rajiv): currently this endpoint is atoms only
@Get("/:bookingUid")
async getBooking(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
async getBooking(@Param("bookingUid") bookingUid: string): Promise<GetBookingOutput> {
const { bookingInfo } = await getBookingInfo(bookingUid);
if (!bookingInfo) {
@@ -95,7 +129,6 @@ export class BookingsController {
};
}
// note(Rajiv): currently this endpoint is atoms only
@Get("/:bookingUid/reschedule")
async getBookingForReschedule(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
const booking = await getBookingForReschedule(bookingUid);
@@ -111,42 +144,48 @@ export class BookingsController {
}
@Post("/")
@Permissions([BOOKING_WRITE])
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);
req.body = { ...req.body, noEmail: true };
const oAuthClientId = clientId?.toString();
const { orgSlug, locationUrl } = body;
req.headers["x-cal-force-slug"] = orgSlug;
try {
const booking = await handleNewBooking(req as unknown as NextApiRequest & { userId?: number });
const booking = await handleNewBooking(
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
);
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
return {
status: SUCCESS_STATUS,
data: booking,
};
} catch (err) {
handleBookingErrors(err);
this.handleBookingErrors(err);
}
throw new InternalServerErrorException("Could not create booking.");
}
@Post("/:bookingId/cancel")
@Permissions([BOOKING_WRITE])
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);
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,
};
} catch (err) {
handleBookingErrors(err);
this.handleBookingErrors(err);
}
} else {
throw new NotFoundException("Booking ID is required.");
@@ -154,51 +193,56 @@ export class BookingsController {
throw new InternalServerErrorException("Could not cancel booking.");
}
@Post("/reccuring")
@Permissions([BOOKING_WRITE])
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);
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)
);
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
return {
status: SUCCESS_STATUS,
data: createdBookings,
};
} catch (err) {
handleBookingErrors(err, "recurring");
this.handleBookingErrors(err, "recurring");
}
throw new InternalServerErrorException("Could not create recurring booking.");
}
@Post("/instant")
@Permissions([BOOKING_WRITE])
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>>>> {
req.userId = await this.getOwnerId(req);
req.body = { ...req.body, noEmail: true };
const oAuthClientId = clientId?.toString();
req.userId = (await this.getOwnerId(req)) ?? -1;
try {
const instantMeeting = await handleInstantMeeting(
req as unknown as NextApiRequest & { userId?: number }
await this.createNextApiBookingRequest(req, oAuthClientId)
);
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
return {
status: SUCCESS_STATUS,
data: instantMeeting,
};
} catch (err) {
handleBookingErrors(err, "instant");
this.handleBookingErrors(err, "instant");
}
throw new InternalServerErrorException("Could not create instant booking.");
}
async getOwnerId(req: Request): Promise<number | undefined> {
private async getOwnerId(req: Request): Promise<number | undefined> {
try {
const accessToken = req.get("Authorization")?.replace("Bearer ", "");
if (accessToken) {
@@ -208,19 +252,52 @@ export class BookingsController {
this.logger.error(err);
}
}
}
function handleBookingErrors(err: Error | HttpError | unknown, type?: "recurring" | `instant`): void {
const errMsg = `Error while creating ${type ? type + " " : ""}booking.`;
if (err instanceof HttpError) {
const httpError = err as HttpError;
throw new HttpException(httpError?.message ?? errMsg, httpError?.statusCode ?? 500);
private async getOAuthClientsParams(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;
}
}
if (err instanceof Error) {
const error = err as Error;
throw new InternalServerErrorException(error?.message ?? errMsg);
private 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(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;
}
throw new InternalServerErrorException(errMsg);
private handleBookingErrors(err: Error | HttpError | unknown, type?: "recurring" | `instant`): void {
const errMsg = `Error while creating ${type ? type + " " : ""}booking.`;
if (err instanceof HttpError) {
const httpError = err as HttpError;
throw new HttpException(httpError?.message ?? errMsg, httpError?.statusCode ?? 500);
}
if (err instanceof Error) {
const error = err as Error;
throw new InternalServerErrorException(error?.message ?? errMsg);
}
throw new InternalServerErrorException(errMsg);
}
}
@@ -1,5 +1,44 @@
import { Transform } from "class-transformer";
import { IsBoolean, IsTimeZone, IsNumber, IsString, IsOptional, IsArray } from "class-validator";
import { Transform, Type } from "class-transformer";
import {
IsBoolean,
IsTimeZone,
IsNumber,
IsString,
IsOptional,
IsArray,
IsObject,
IsEmail,
ValidateNested,
} from "class-validator";
class Location {
@IsString()
optionValue!: string;
@IsString()
value!: string;
}
class Response {
@IsString()
name!: string;
@IsEmail()
email!: string;
@IsArray()
@IsString({ each: true })
guests!: string[];
@IsOptional()
@ValidateNested()
@Type(() => Location)
location?: Location;
@IsOptional()
@IsString()
notes?: string;
}
export class CreateBookingInput {
@IsString()
@@ -42,6 +81,7 @@ export class CreateBookingInput {
@IsOptional()
bookingUid?: string;
@IsObject()
metadata!: Record<string, string>;
@IsBoolean()
@@ -55,4 +95,15 @@ export class CreateBookingInput {
@IsString()
@IsOptional()
seatReferenceUid?: string;
@Type(() => Response)
responses!: Response;
@IsString()
@IsOptional()
orgSlug?: string;
@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;
@@ -0,0 +1,173 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsString,
IsEnum,
IsInt,
IsOptional,
IsObject,
ValidateNested,
IsArray,
IsUrl,
IsDateString,
IsEmail,
} from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Metadata {
@IsUrl()
videoCallUrl!: string;
}
class Location {
@IsString()
optionValue!: string;
@IsString()
value!: string;
}
class Response {
@IsString()
name!: string;
@IsEmail()
email!: string;
@IsString()
notes!: string;
@IsArray()
@IsString({ each: true })
guests!: string[];
@ValidateNested()
@Type(() => Location)
location!: Location;
}
class User {
@IsInt()
id!: number;
@IsString()
name!: string | null;
@IsEmail()
email!: string;
@IsString()
username!: string | null;
@IsString()
timeZone!: string;
}
class Attendee {
@IsString()
name!: string;
@IsEmail()
email!: string;
@IsString()
timeZone!: string;
}
class EventType {
@IsOptional()
@IsString()
eventName!: string | null;
@IsString()
slug!: string;
@IsOptional()
@IsString()
timeZone!: string | null;
}
class GetBookingData {
@IsString()
title!: string;
@IsInt()
id!: number;
@IsString()
uid!: string;
@IsString()
description!: string | null;
@IsObject()
customInputs!: any;
@IsOptional()
@IsString()
smsReminderNumber!: string | null;
@IsOptional()
@IsString()
recurringEventId!: string | null;
@IsDateString()
startTime!: Date;
@IsDateString()
endTime!: Date;
@IsUrl()
location!: string | null;
@IsString()
status!: string;
metadata!: Metadata | any;
@IsOptional()
@IsString()
cancellationReason!: string | null;
@ValidateNested()
@Type(() => Response)
responses!: Response | any;
@IsOptional()
@IsString()
rejectionReason!: string | null;
@IsString()
@IsEmail()
userPrimaryEmail!: string | null;
@ValidateNested()
@Type(() => User)
user!: User | null;
@ValidateNested()
@Type(() => Attendee)
@IsArray()
attendees!: Attendee[];
@IsInt()
eventTypeId!: number | null;
@ValidateNested()
@Type(() => EventType)
eventType!: EventType | null;
}
export class GetBookingOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: GetBookingData,
})
@ValidateNested()
@Type(() => GetBookingData)
data!: GetBookingData;
}
@@ -0,0 +1,229 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsString,
IsEnum,
IsInt,
IsBoolean,
IsUrl,
IsOptional,
IsObject,
ValidateNested,
IsArray,
IsDateString,
IsEmail,
} from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
enum Status {
CANCELLED = "CANCELLED",
REJECTED = "REJECTED",
ACCEPTED = "ACCEPTED",
PENDING = "PENDING",
AWAITING_HOST = "AWAITING_HOST",
}
class Attendee {
@IsInt()
id!: number;
@IsEmail()
email!: string;
@IsString()
name!: string;
@IsString()
timeZone!: string;
@IsString()
locale!: string | null;
@IsInt()
bookingId!: number | null;
}
class EventType {
@IsString()
@IsOptional()
slug?: string;
@IsOptional()
@IsInt()
id?: number;
@IsOptional()
@IsString()
eventName?: string | null;
@IsInt()
price!: number;
@IsOptional()
recurringEvent?: any;
@IsString()
currency!: string;
@IsObject()
metadata!: any;
@IsBoolean()
@IsOptional()
seatsShowAttendees?: boolean | undefined | null;
@IsBoolean()
@IsOptional()
seatsShowAvailabilityCount?: boolean | undefined | null;
@IsOptional()
team?: any | null;
}
class Reference {
@IsInt()
id!: number;
@IsString()
type!: string;
@IsString()
uid!: string;
@IsOptional()
@IsString()
meetingId?: string | null;
@IsOptional()
@IsString()
thirdPartyRecurringEventId?: string | null;
@IsString()
meetingPassword!: string | null;
@IsOptional()
@IsString()
meetingUrl?: string | null;
@IsInt()
bookingId!: number | null;
@IsEmail()
externalCalendarId!: string | null;
@IsOptional()
deleted?: any;
@IsInt()
credentialId!: number | null;
}
class User {
@IsInt()
id!: number;
@IsString()
name!: string | null;
@IsEmail()
email!: string;
}
class GetBookingsDataEntry {
@IsInt()
id!: number;
@IsString()
title!: string;
@IsOptional()
@IsEmail()
userPrimaryEmail?: string | null;
@IsString()
description!: string | null;
@IsObject()
customInputs!: object | any;
@IsDateString()
startTime!: string;
@IsDateString()
endTime!: string;
@ValidateNested({ each: true })
@Type(() => Attendee)
@IsArray()
attendees!: Attendee[];
metadata!: any;
@IsString()
uid!: string;
@IsOptional()
@IsString()
recurringEventId!: string | null;
@IsUrl()
location!: string | null;
@ValidateNested()
@Type(() => EventType)
eventType!: EventType;
@IsEnum(Status)
status!: "CANCELLED" | "REJECTED" | "ACCEPTED" | "PENDING" | "AWAITING_HOST";
@IsBoolean()
paid!: boolean;
@IsArray()
payment!: any[];
@ValidateNested()
@Type(() => Reference)
@IsArray()
references!: Reference[];
@IsBoolean()
isRecorded!: boolean;
@IsArray()
seatsReferences!: any[];
@ValidateNested()
@Type(() => User)
user!: User | null;
@IsOptional()
rescheduled?: any;
}
class GetBookingsData {
@ValidateNested()
@Type(() => GetBookingsDataEntry)
@IsArray()
bookings!: GetBookingsDataEntry[];
@IsArray()
recurringInfo!: any[];
@IsInt()
nextCursor!: number | null;
}
export class GetBookingsOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: GetBookingsData,
})
@ValidateNested()
@Type(() => GetBookingsData)
data!: GetBookingsData;
}
@@ -1,20 +1,21 @@
import { GetBusyTimesOutput } from "@/ee/calendars/outputs/busy-times.output";
import { ConnectedCalendarsOutput } from "@/ee/calendars/outputs/connected-calendars.output";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Controller, Get, UseGuards, Query } from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ConnectedDestinationCalendars } from "@calcom/platform-libraries";
import { CalendarBusyTimesInput } from "@calcom/platform-types";
import { ApiResponse } from "@calcom/platform-types";
import { EventBusyDate } from "@calcom/types/Calendar";
@Controller({
path: "ee/calendars",
path: "/calendars",
version: "2",
})
@UseGuards(AccessTokenGuard)
@DocsTags("Calendars")
export class CalendarsController {
constructor(private readonly calendarsService: CalendarsService) {}
@@ -22,7 +23,7 @@ export class CalendarsController {
async getBusyTimes(
@Query() queryParams: CalendarBusyTimesInput,
@GetUser() user: UserWithProfile
): Promise<ApiResponse<EventBusyDate[]>> {
): Promise<GetBusyTimesOutput> {
const { loggedInUsersTz, dateFrom, dateTo, calendarsToLoad } = queryParams;
if (!dateFrom || !dateTo) {
return {
@@ -46,7 +47,7 @@ export class CalendarsController {
}
@Get("/")
async getCalendars(@GetUser("id") userId: number): Promise<ApiResponse<ConnectedDestinationCalendars>> {
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
const calendars = await this.calendarsService.getCalendars(userId);
return {
@@ -0,0 +1,28 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsDate, IsEnum, IsOptional, IsString, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class BusyTimesOutput {
@IsDate()
start!: Date;
@IsDate()
end!: Date;
@IsOptional()
@IsString()
source?: string | null;
}
export class GetBusyTimesOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ValidateNested()
@Type(() => BusyTimesOutput)
@IsArray()
data!: BusyTimesOutput[];
}
@@ -0,0 +1,218 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsArray,
IsBoolean,
IsEmail,
IsEnum,
IsInt,
IsObject,
IsOptional,
IsString,
IsUrl,
ValidateNested,
} from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Integration {
@IsOptional()
@IsObject()
appData?: object | null;
@IsOptional()
@IsString()
dirName?: string;
@IsOptional()
@IsString()
__template?: string;
@IsString()
name!: string;
@IsString()
description!: string;
@IsOptional()
@IsBoolean()
installed?: boolean;
@IsString()
type!: string;
@IsOptional()
@IsString()
title?: string;
@IsString()
variant!: string;
@IsOptional()
@IsString()
category?: string;
@IsArray()
@IsString({ each: true })
categories!: string[];
@IsString()
logo!: string;
@IsString()
publisher!: string;
@IsString()
slug!: string;
@IsUrl()
url!: string;
@IsEmail()
email!: string;
@IsOptional()
@IsObject()
locationOption!: object | null;
}
class Primary {
@IsEmail()
externalId!: string;
@IsString()
@IsOptional()
integration?: string;
@IsOptional()
@IsEmail()
name?: string;
@IsBoolean()
primary!: boolean | null;
@IsBoolean()
readOnly!: boolean;
@IsEmail()
@IsOptional()
email?: string;
@IsBoolean()
isSelected!: boolean;
@IsInt()
credentialId!: number;
}
class Calendar {
@IsEmail()
externalId!: string;
@IsString()
@IsOptional()
integration?: string;
@IsEmail()
@IsOptional()
name?: string;
@IsOptional()
@IsBoolean()
primary?: boolean | null;
@IsBoolean()
readOnly!: boolean;
@IsEmail()
@IsOptional()
email?: string;
@IsBoolean()
isSelected!: boolean;
@IsInt()
credentialId!: number;
}
class ConnectedCalendar {
@ValidateNested()
@IsObject()
integration!: Integration;
@IsInt()
credentialId!: number;
@ValidateNested()
@IsObject()
@IsOptional()
primary?: Primary;
@ValidateNested({ each: true })
@IsArray()
@IsOptional()
calendars?: Calendar[];
}
class DestinationCalendar {
@IsInt()
id!: number;
@IsString()
integration!: string;
@IsEmail()
externalId!: string;
@IsEmail()
primaryEmail!: string | null;
@IsInt()
userId!: number | null;
@IsOptional()
@IsInt()
eventTypeId!: number | null;
@IsInt()
credentialId!: number | null;
@IsString()
@IsOptional()
name?: string | null;
@IsBoolean()
@IsOptional()
primary?: boolean;
@IsBoolean()
@IsOptional()
readOnly?: boolean;
@IsEmail()
@IsOptional()
email?: string;
@IsString()
@IsOptional()
integrationTitle?: string;
}
class ConnectedCalendarsData {
@ValidateNested({ each: true })
@IsArray()
connectedCalendars!: ConnectedCalendar[];
@ValidateNested()
@IsObject()
destinationCalendar!: DestinationCalendar;
}
export class ConnectedCalendarsOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ValidateNested()
@Type(() => ConnectedCalendarsData)
data!: ConnectedCalendarsData;
}
@@ -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,6 +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";
@@ -63,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;
@@ -172,14 +175,49 @@ describe("Event types Endpoints", () => {
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: ApiSuccessResponse<EventType> = response.body;
const responseBody: GetEventTypeOutput = 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.userId).toEqual(user.id);
expect(responseBody.data.eventType.id).toEqual(eventType.id);
expect(responseBody.data.eventType.title).toEqual(eventType.title);
expect(responseBody.data.eventType.slug).toEqual(eventType.slug);
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 () => {
@@ -201,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,16 +1,19 @@
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";
import { GetEventTypesData, GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
import { UpdateEventTypeOutput } from "@/ee/event-types/outputs/update-event-type.output";
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
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])
@@ -86,12 +96,40 @@ export class EventTypesController {
return {
status: SUCCESS_STATUS,
data: eventTypes,
data: eventTypes as GetEventTypesData,
};
}
@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);
@@ -2,9 +2,10 @@ import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { getEventTypeById } from "@calcom/platform-libraries";
import type { PrismaClient } from "@calcom/prisma";
@Injectable()
export class EventTypesRepository {
@@ -44,19 +45,14 @@ export class EventTypesRepository {
isUserOrganizationAdmin: boolean,
eventTypeId: number
) {
try {
return getEventTypeById({
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,
isUserOrganizationAdmin,
});
} catch (error) {
throw new NotFoundException(`User with id ${user.id} has no event type with id ${eventTypeId}`);
}
return await getEventTypeById({
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
eventTypeId,
userId: user.id,
prisma: this.dbRead.prisma as unknown as PrismaClient,
isUserOrganizationAdmin,
isTrpcCall: true,
});
}
async getEventTypeById(eventTypeId: number) {
@@ -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()
@@ -21,5 +12,5 @@ export class GetPublicEventInput {
@ApiProperty({ required: false })
@IsString()
@IsOptional()
org?: string;
org?: string | null;
}
@@ -55,11 +55,12 @@ export class EventTypeOutput {
@ValidateNested({ each: true })
@Type(() => EventTypeLocation)
@IsArray()
locations!: EventTypeLocation | null;
locations!: EventTypeLocation[] | null;
@IsInt()
@ApiHideProperty()
position!: number;
@IsOptional()
position?: number;
@IsInt()
@ApiHideProperty()
@@ -71,7 +72,8 @@ export class EventTypeOutput {
@IsInt()
@ApiHideProperty()
profileId!: number | null;
@IsOptional()
profileId?: number | null;
@IsInt()
@ApiHideProperty()
@@ -83,7 +85,8 @@ export class EventTypeOutput {
@IsInt()
@ApiHideProperty()
parentId!: number | null;
@IsOptional()
parentId?: number | null;
@IsOptional()
@IsArray()
@@ -176,7 +179,8 @@ export class EventTypeOutput {
@IsInt()
@ApiHideProperty()
scheduleId!: number | null;
@IsOptional()
scheduleId?: number | null;
@IsNumber()
@ApiHideProperty()
@@ -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;
}
@@ -12,7 +12,7 @@ class EventTypeGroup {
eventTypes!: EventTypeOutput[];
}
class GetEventTypesData {
export class GetEventTypesData {
@ValidateNested({ each: true })
@Type(() => EventTypeGroup)
@IsArray()
@@ -2,6 +2,7 @@ import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/constants/constants";
import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
@@ -22,7 +23,7 @@ export class EventTypesService {
private readonly dbWrite: PrismaWriteService
) {}
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput) {
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput): Promise<EventTypeOutput> {
await this.checkCanCreateEventType(user.id, body);
const eventTypeUser = await this.getUserToCreateEvent(user);
const { eventType } = await createEventType({
@@ -34,7 +35,7 @@ export class EventTypesService {
prisma: this.dbWrite.prisma,
},
});
return eventType;
return eventType as EventTypeOutput;
}
async checkCanCreateEventType(userId: number, body: CreateEventTypeInput) {
@@ -88,7 +89,7 @@ export class EventTypesService {
}
this.checkUserOwnsEventType(user.id, eventType.eventType);
return eventType;
return eventType as { eventType: EventTypeOutput };
}
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
@@ -126,9 +127,13 @@ export class EventTypesService {
},
});
const { eventType } = await this.getUserEventTypeForAtom(user, eventTypeId);
const eventType = await this.getUserEventTypeForAtom(user, eventTypeId);
return eventType;
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
return eventType.eventType;
}
async checkCanUpdateEventType(userId: number, eventTypeId: number) {
@@ -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);
+29 -9
View File
@@ -1,3 +1,7 @@
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GcalAuthUrlOutput } from "@/ee/gcal/outputs/auth-url.output";
import { GcalCheckOutput } from "@/ee/gcal/outputs/check.output";
import { GcalSaveRedirectOutput } from "@/ee/gcal/outputs/save-redirect.output";
import { GCalService } from "@/modules/apps/services/gcal.service";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
@@ -21,6 +25,8 @@ import {
Headers,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { Prisma } from "@prisma/client";
import { Request } from "express";
import { google } from "googleapis";
import { z } from "zod";
@@ -31,17 +37,18 @@ import {
GOOGLE_CALENDAR_TYPE,
SUCCESS_STATUS,
} from "@calcom/platform-constants";
import { ApiRedirectResponseType, ApiResponse } from "@calcom/platform-types";
const CALENDAR_SCOPES = [
"https://www.googleapis.com/auth/calendar.readonly",
"https://www.googleapis.com/auth/calendar.events",
];
// Controller for the GCalConnect Atom
@Controller({
path: "ee/gcal",
path: "/gcal",
version: "2",
})
@DocsTags("Google Calendar")
export class GcalController {
private readonly logger = new Logger("Platform Gcal Provider");
@@ -50,10 +57,11 @@ export class GcalController {
private readonly tokensRepository: TokensRepository,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
private readonly config: ConfigService,
private readonly gcalService: GCalService
private readonly gcalService: GCalService,
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)
@@ -61,7 +69,7 @@ export class GcalController {
async redirect(
@Headers("Authorization") authorization: string,
@Req() req: Request
): Promise<ApiResponse<{ authUrl: string }>> {
): Promise<GcalAuthUrlOutput> {
const oAuth2Client = await this.gcalService.getOAuthClient(this.redirectUri);
const accessToken = authorization.replace("Bearer ", "");
const origin = req.get("origin") ?? req.get("host");
@@ -77,7 +85,7 @@ export class GcalController {
@Get("/oauth/save")
@Redirect(undefined, 301)
@HttpCode(HttpStatus.OK)
async save(@Query("state") state: string, @Query("code") code: string): Promise<ApiRedirectResponseType> {
async save(@Query("state") state: string, @Query("code") code: string): Promise<GcalSaveRedirectOutput> {
const stateParams = new URLSearchParams(state);
const { accessToken, origin } = z
.object({ accessToken: z.string(), origin: z.string() })
@@ -99,10 +107,11 @@ export class GcalController {
const oAuth2Client = await this.gcalService.getOAuthClient(this.redirectUri);
const token = await oAuth2Client.getToken(parsedCode);
const key = token.res?.data;
// Google oAuth Credentials are stored in token.tokens
const key = token.tokens;
const credential = await this.credentialRepository.createAppCredential(
GOOGLE_CALENDAR_TYPE,
key,
key as Prisma.InputJsonValue,
ownerId
);
@@ -133,7 +142,7 @@ export class GcalController {
@HttpCode(HttpStatus.OK)
@UseGuards(AccessTokenGuard, PermissionsGuard)
@Permissions([APPS_READ])
async check(@GetUser("id") userId: number): Promise<ApiResponse> {
async check(@GetUser("id") userId: number): Promise<GcalCheckOutput> {
const gcalCredentials = await this.credentialRepository.getByTypeAndUserId("google_calendar", userId);
if (!gcalCredentials) {
@@ -144,6 +153,17 @@ export class GcalController {
throw new BadRequestException("Invalid google oauth credentials.");
}
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
const googleCalendar = connectedCalendars.find(
(cal: { integration: { type: string } }) => cal.integration.type === GOOGLE_CALENDAR_TYPE
);
if (!googleCalendar) {
throw new UnauthorizedException("Google Calendar not connected.");
}
if (googleCalendar.error?.message) {
throw new UnauthorizedException(googleCalendar.error?.message);
}
return { status: SUCCESS_STATUS };
}
}
+11 -1
View File
@@ -1,3 +1,4 @@
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GcalController } from "@/ee/gcal/gcal.controller";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { GCalService } from "@/modules/apps/services/gcal.service";
@@ -6,12 +7,21 @@ import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
@Module({
imports: [PrismaModule, TokensModule, OAuthClientModule],
providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GCalService],
providers: [
AppsRepository,
ConfigService,
CredentialsRepository,
SelectedCalendarsRepository,
GCalService,
CalendarsService,
UsersRepository,
],
controllers: [GcalController],
})
export class GcalModule {}
@@ -0,0 +1,20 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsString, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class AuthUrlData {
@IsString()
authUrl!: string;
}
export class GcalAuthUrlOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@Type(() => AuthUrlData)
@ValidateNested()
data!: AuthUrlData;
}
@@ -0,0 +1,10 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class GcalCheckOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
}
@@ -0,0 +1,6 @@
import { IsString } from "class-validator";
export class GcalSaveRedirectOutput {
@IsString()
url!: string;
}
+20 -17
View File
@@ -1,12 +1,11 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
import { SchedulesModule } from "@/ee/schedules/schedules.module";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
@@ -35,8 +34,14 @@ describe("Me Endpoints", () => {
const moduleRef = await withAccessTokenAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule, TokensModule],
providers: [SchedulesRepository, SchedulesService],
imports: [
AppModule,
PrismaModule,
AvailabilitiesModule,
UsersModule,
TokensModule,
SchedulesModule,
],
})
)
.overrideGuard(PermissionsGuard)
@@ -50,6 +55,7 @@ describe("Me Endpoints", () => {
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
});
app = moduleRef.createNestApplication();
@@ -65,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;
@@ -81,10 +87,10 @@ describe("Me Endpoints", () => {
});
it("should update user associated with access token", async () => {
const body: UpdateManagedPlatformUserInput = { timeZone: "Europe/Rome" };
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) => {
@@ -106,24 +112,21 @@ describe("Me Endpoints", () => {
});
it("should not update user associated with access token given invalid timezone", async () => {
const bodyWithIncorrectTimeZone: UpdateManagedPlatformUserInput = { timeZone: "Narnia/Woods" };
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: UpdateManagedPlatformUserInput = { timeFormat: 100 };
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: UpdateManagedPlatformUserInput = { weekStart: "waba luba dub dub" };
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 () => {
+11 -8
View File
@@ -1,21 +1,24 @@
import { GetMeOutput } from "@/ee/me/outputs/get-me.output";
import { UpdateMeOutput } from "@/ee/me/outputs/update-me.output";
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
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 { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
import { Controller, UseGuards, Get, Patch, Body } from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { PROFILE_READ, PROFILE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { UserResponse, userSchemaResponse } from "@calcom/platform-types";
import { ApiResponse } from "@calcom/platform-types";
import { userSchemaResponse } from "@calcom/platform-types";
@Controller({
path: "ee/me",
path: "/me",
version: "2",
})
@UseGuards(AccessTokenGuard, PermissionsGuard)
@DocsTags("Me")
export class MeController {
constructor(
private readonly usersRepository: UsersRepository,
@@ -24,7 +27,7 @@ export class MeController {
@Get("/")
@Permissions([PROFILE_READ])
async getMe(@GetUser() user: UserWithProfile): Promise<ApiResponse<UserResponse>> {
async getMe(@GetUser() user: UserWithProfile): Promise<GetMeOutput> {
const me = userSchemaResponse.parse(user);
return {
@@ -37,11 +40,11 @@ export class MeController {
@Permissions([PROFILE_WRITE])
async updateMe(
@GetUser() user: UserWithProfile,
@Body() bodySchedule: UpdateManagedPlatformUserInput
): Promise<ApiResponse<UserResponse>> {
@Body() bodySchedule: UpdateManagedUserInput
): Promise<UpdateMeOutput> {
const updatedUser = await this.usersRepository.update(user.id, bodySchedule);
if (bodySchedule.timeZone && user.defaultScheduleId) {
await this.schedulesRepository.updateUserSchedule(user.id, user.defaultScheduleId, {
await this.schedulesRepository.updateUserSchedule(user, user.defaultScheduleId, {
timeZone: bodySchedule.timeZone,
});
}
@@ -0,0 +1,20 @@
import { MeOutput } from "@/ee/me/outputs/me.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class GetMeOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: MeOutput,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => MeOutput)
data!: MeOutput;
}
@@ -0,0 +1,25 @@
import { IsInt, IsEmail, IsOptional, IsString } from "class-validator";
export class MeOutput {
@IsInt()
id!: number;
@IsString()
username!: string;
@IsEmail()
email!: string;
@IsInt()
timeFormat!: number;
@IsInt()
@IsOptional()
defaultScheduleId!: number | null;
@IsString()
weekStart!: string;
@IsString()
timeZone!: string;
}
@@ -0,0 +1,20 @@
import { MeOutput } from "@/ee/me/outputs/me.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class UpdateMeOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: MeOutput,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => MeOutput)
data!: MeOutput;
}
@@ -0,0 +1,10 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class ProviderVerifyAccessTokenOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
}
@@ -0,0 +1,10 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class ProviderVerifyClientOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
}
@@ -1,3 +1,5 @@
import { ProviderVerifyAccessTokenOutput } from "@/ee/provider/outputs/verify-access-token.output";
import { ProviderVerifyClientOutput } from "@/ee/provider/outputs/verify-client.output";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
@@ -13,20 +15,21 @@ import {
UnauthorizedException,
UseGuards,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ApiResponse } from "@calcom/platform-types";
@Controller({
path: "ee/provider",
path: "/provider",
version: "2",
})
@DocsTags("Cal provider")
export class CalProviderController {
constructor(private readonly oauthClientRepository: OAuthClientRepository) {}
@Get("/:clientId")
@HttpCode(HttpStatus.OK)
async verifyClientId(@Param("clientId") clientId: string): Promise<ApiResponse> {
async verifyClientId(@Param("clientId") clientId: string): Promise<ProviderVerifyClientOutput> {
if (!clientId) {
throw new NotFoundException();
}
@@ -45,7 +48,7 @@ export class CalProviderController {
async verifyAccessToken(
@Param("clientId") clientId: string,
@GetUser() user: UserWithProfile
): Promise<ApiResponse> {
): Promise<ProviderVerifyAccessTokenOutput> {
if (!clientId) {
throw new BadRequestException();
}
@@ -1,7 +1,10 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output";
import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output";
import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output";
import { SchedulesModule } from "@/ee/schedules/schedules.module";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
@@ -17,13 +20,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import {
ScheduleWithAvailabilities,
ScheduleWithAvailabilitiesForWeb,
UpdateScheduleOutputType,
} from "@calcom/platform-libraries";
import { ScheduleResponse, UpdateScheduleInput } from "@calcom/platform-types";
import { ApiSuccessResponse } from "@calcom/platform-types";
import { UpdateScheduleInput } from "@calcom/platform-types";
describe("Schedules Endpoints", () => {
describe("User Authentication", () => {
@@ -35,14 +32,23 @@ describe("Schedules Endpoints", () => {
const userEmail = "schedules-controller-e2e@api.com";
let user: User;
let createdSchedule: ScheduleResponse;
let createdSchedule: CreateScheduleOutput["data"];
const defaultAvailabilityDays = [1, 2, 3, 4, 5];
const defaultAvailabilityStartTime = "1970-01-01T09:00:00.000Z";
const defaultAvailabilityEndTime = "1970-01-01T17:00:00.000Z";
beforeAll(async () => {
const moduleRef = await withAccessTokenAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule, TokensModule],
providers: [SchedulesRepository, SchedulesService],
imports: [
AppModule,
PrismaModule,
AvailabilitiesModule,
UsersModule,
TokensModule,
SchedulesModule,
],
})
)
.overrideGuard(PermissionsGuard)
@@ -71,10 +77,12 @@ describe("Schedules Endpoints", () => {
it("should create a default schedule", async () => {
const scheduleName = "schedule-name";
const scheduleTimeZone = "Europe/Rome";
const isDefault = true;
const body = {
const body: CreateScheduleInput = {
name: scheduleName,
timeZone: scheduleTimeZone,
isDefault,
};
return request(app.getHttpServer())
@@ -82,73 +90,25 @@ describe("Schedules Endpoints", () => {
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<ScheduleResponse> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.id).toBeDefined();
expect(responseBody.data.userId).toEqual(user.id);
expect(responseBody.data.name).toEqual(scheduleName);
expect(responseBody.data.timeZone).toEqual(scheduleTimeZone);
const responseData: CreateScheduleOutput = response.body;
expect(responseData.status).toEqual(SUCCESS_STATUS);
expect(responseData.data).toBeDefined();
expect(responseData.data.isDefault).toEqual(isDefault);
expect(responseData.data.timeZone).toEqual(scheduleTimeZone);
expect(responseData.data.name).toEqual(scheduleName);
expect(responseBody.data.availability).toBeDefined();
expect(responseBody.data.availability?.length).toEqual(1);
const defaultAvailabilityDays = [1, 2, 3, 4, 5];
const defaultAvailabilityStartTime = "09:00:00";
const defaultAvailabilityEndTime = "17:00:00";
const schedule = responseData.data.schedule;
expect(schedule).toBeDefined();
expect(schedule.length).toEqual(1);
expect(schedule?.[0]?.days).toEqual(defaultAvailabilityDays);
expect(schedule?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
expect(schedule?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
expect(responseBody.data.availability?.[0]?.days).toEqual(defaultAvailabilityDays);
expect(responseBody.data.availability?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
expect(responseBody.data.availability?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
const scheduleUser = await userRepositoryFixture.get(responseBody.data.userId);
expect(scheduleUser?.defaultScheduleId).toEqual(responseBody.data.id);
await scheduleRepositoryFixture.deleteById(responseBody.data.id);
await scheduleRepositoryFixture.deleteAvailabilities(responseBody.data.id);
});
});
it("should create a schedule", async () => {
const scheduleName = "schedule-name";
const scheduleTimeZone = "Europe/Rome";
const availabilityDays = [1, 2, 3, 4, 5, 6];
const availabilityStartTime = "11:00:00";
const availabilityEndTime = "14:00:00";
const body = {
name: scheduleName,
timeZone: scheduleTimeZone,
availabilities: [
{
days: availabilityDays,
startTime: availabilityStartTime,
endTime: availabilityEndTime,
},
],
};
return request(app.getHttpServer())
.post("/api/v2/schedules")
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<ScheduleResponse> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.id).toBeDefined();
expect(responseBody.data.userId).toEqual(user.id);
expect(responseBody.data.name).toEqual(scheduleName);
expect(responseBody.data.timeZone).toEqual(scheduleTimeZone);
expect(responseBody.data.availability).toBeDefined();
expect(responseBody.data.availability?.length).toEqual(1);
expect(responseBody.data.availability?.[0]?.days).toEqual(availabilityDays);
expect(responseBody.data.availability?.[0]?.startTime).toEqual(availabilityStartTime);
expect(responseBody.data.availability?.[0]?.endTime).toEqual(availabilityEndTime);
createdSchedule = responseBody.data;
const scheduleUser = await userRepositoryFixture.get(responseBody.data.userId);
expect(scheduleUser?.defaultScheduleId).toEqual(responseBody.data.id);
const scheduleUser = schedule?.[0].userId
? await userRepositoryFixture.get(schedule?.[0].userId)
: null;
expect(scheduleUser?.defaultScheduleId).toEqual(responseData.data.id);
createdSchedule = responseData.data;
});
});
@@ -156,90 +116,19 @@ describe("Schedules Endpoints", () => {
return request(app.getHttpServer())
.get("/api/v2/schedules/default")
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ScheduleResponse> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.id).toBeDefined();
expect(responseBody.data.userId).toEqual(createdSchedule.userId);
expect(responseBody.data.name).toEqual(createdSchedule.name);
expect(responseBody.data.timeZone).toEqual(createdSchedule.timeZone);
.then(async (response) => {
const responseData: CreateScheduleOutput = response.body;
expect(responseData.status).toEqual(SUCCESS_STATUS);
expect(responseData.data).toBeDefined();
expect(responseData.data.id).toEqual(createdSchedule.id);
expect(responseData.data.schedule?.[0].userId).toEqual(createdSchedule.schedule[0].userId);
expect(responseBody.data.availability).toBeDefined();
expect(responseBody.data.availability?.length).toEqual(1);
expect(responseBody.data.availability?.[0]?.days).toEqual(createdSchedule.availability?.[0]?.days);
expect(responseBody.data.availability?.[0]?.startTime).toEqual(
createdSchedule.availability?.[0]?.startTime
);
expect(responseBody.data.availability?.[0]?.endTime).toEqual(
createdSchedule.availability?.[0]?.endTime
);
});
});
it("should get schedule", async () => {
return request(app.getHttpServer())
.get(`/api/v2/schedules/${createdSchedule.id}`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ScheduleResponse> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.id).toBeDefined();
expect(
(responseBody.data as unknown as ScheduleWithAvailabilitiesForWeb).dateOverrides
).toBeFalsy();
expect(responseBody.data.userId).toEqual(createdSchedule.userId);
expect(responseBody.data.name).toEqual(createdSchedule.name);
expect(responseBody.data.timeZone).toEqual(createdSchedule.timeZone);
expect(responseBody.data.availability).toBeDefined();
expect(responseBody.data.availability?.length).toEqual(1);
expect(responseBody.data.availability?.[0]?.days).toEqual(createdSchedule.availability?.[0]?.days);
expect(responseBody.data.availability?.[0]?.startTime).toEqual(
createdSchedule.availability?.[0]?.startTime
);
expect(responseBody.data.availability?.[0]?.endTime).toEqual(
createdSchedule.availability?.[0]?.endTime
);
});
});
it("should get schedule for atom", async () => {
const forAtomQueryParam = "?for=atom";
return request(app.getHttpServer())
.get(`/api/v2/schedules/${createdSchedule.id}${forAtomQueryParam}`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ScheduleWithAvailabilitiesForWeb> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.id).toBeDefined();
expect(responseBody.data.dateOverrides).toBeDefined();
expect((responseBody.data as unknown as ScheduleWithAvailabilities).userId).toBeFalsy();
expect(responseBody.data.name).toEqual(createdSchedule.name);
expect(responseBody.data.timeZone).toEqual(createdSchedule.timeZone);
expect(responseBody.data.availability).toBeDefined();
expect(responseBody.data.availability?.length).toEqual(7);
const dateStart = new Date(responseBody.data.availability?.[1]?.[0]?.start);
const dateEnd = new Date(responseBody.data.availability?.[1]?.[0]?.end);
const dateStartHours = dateStart.getUTCHours().toString().padStart(2, "0");
const dateStartMinutes = dateStart.getUTCMinutes().toString().padStart(2, "0");
const dateStartSeconds = dateStart.getUTCSeconds().toString().padStart(2, "0");
const dateEndHours = dateEnd.getUTCHours().toString().padStart(2, "0");
const dateEndMinutes = dateEnd.getUTCMinutes().toString().padStart(2, "0");
const dateEndSeconds = dateEnd.getUTCSeconds().toString().padStart(2, "0");
const expectedStart = `${dateStartHours}:${dateStartMinutes}:${dateStartSeconds}`;
const expectedEnd = `${dateEndHours}:${dateEndMinutes}:${dateEndSeconds}`;
expect(expectedStart).toEqual(createdSchedule.availability?.[0]?.startTime);
expect(expectedEnd).toEqual(createdSchedule.availability?.[0]?.endTime);
const schedule = responseData.data.schedule;
expect(schedule).toBeDefined();
expect(schedule.length).toEqual(1);
expect(schedule?.[0]?.days).toEqual(defaultAvailabilityDays);
expect(schedule?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
expect(schedule?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
});
});
@@ -248,77 +137,26 @@ describe("Schedules Endpoints", () => {
.get(`/api/v2/schedules`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ScheduleResponse[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.length).toEqual(1);
const responseData: GetSchedulesOutput = response.body;
expect(responseData.status).toEqual(SUCCESS_STATUS);
expect(responseData.data).toBeDefined();
expect(responseData.data?.[0].id).toEqual(createdSchedule.id);
expect(responseData.data?.[0].schedule?.[0].userId).toEqual(createdSchedule.schedule[0].userId);
const fetchedSchedule = responseBody.data[0];
expect(fetchedSchedule).toBeDefined();
expect(fetchedSchedule.userId).toEqual(createdSchedule.userId);
expect(fetchedSchedule.name).toEqual(createdSchedule.name);
expect(fetchedSchedule.timeZone).toEqual(createdSchedule.timeZone);
expect(fetchedSchedule.availability).toBeDefined();
expect(fetchedSchedule.availability?.length).toEqual(1);
expect(fetchedSchedule.availability?.[0]?.days).toEqual(createdSchedule.availability?.[0]?.days);
expect(fetchedSchedule.availability?.[0]?.startTime).toEqual(
createdSchedule.availability?.[0]?.startTime
);
expect(fetchedSchedule.availability?.[0]?.endTime).toEqual(
createdSchedule.availability?.[0]?.endTime
);
const schedule = responseData.data?.[0].schedule;
expect(schedule).toBeDefined();
expect(schedule.length).toEqual(1);
expect(schedule?.[0]?.days).toEqual(defaultAvailabilityDays);
expect(schedule?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
expect(schedule?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
});
});
it("should update schedule name", async () => {
const newScheduleName = "new-schedule-name";
const schedule = [
[],
[
{
start: new Date("2024-03-05T11:00:00.000Z"),
end: new Date("2024-03-05T14:00:00.000Z"),
},
],
[
{
start: new Date("2024-03-05T11:00:00.000Z"),
end: new Date("2024-03-05T14:00:00.000Z"),
},
],
[
{
start: new Date("2024-03-05T11:00:00.000Z"),
end: new Date("2024-03-05T14:00:00.000Z"),
},
],
[
{
start: new Date("2024-03-05T11:00:00.000Z"),
end: new Date("2024-03-05T14:00:00.000Z"),
},
],
[
{
start: new Date("2024-03-05T11:00:00.000Z"),
end: new Date("2024-03-05T14:00:00.000Z"),
},
],
[
{
start: new Date("2024-03-05T11:00:00.000Z"),
end: new Date("2024-03-05T14:00:00.000Z"),
},
],
];
const body: UpdateScheduleInput = {
scheduleId: createdSchedule.id,
name: newScheduleName,
schedule,
};
return request(app.getHttpServer())
@@ -326,29 +164,19 @@ describe("Schedules Endpoints", () => {
.send(body)
.expect(200)
.then((response: any) => {
const responseBody: ApiSuccessResponse<UpdateScheduleOutputType> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.schedule.id).toBeDefined();
expect(responseBody.data.schedule.userId).toEqual(createdSchedule.userId);
expect(responseBody.data.schedule.name).toEqual(newScheduleName);
expect(responseBody.data.timeZone).toEqual(createdSchedule.timeZone);
const responseData: UpdateScheduleOutput = response.body;
expect(responseData.status).toEqual(SUCCESS_STATUS);
expect(responseData.data).toBeDefined();
expect(responseData.data.schedule.name).toEqual(newScheduleName);
expect(responseData.data.schedule.id).toEqual(createdSchedule.id);
expect(responseData.data.schedule.userId).toEqual(createdSchedule.schedule[0].userId);
expect(responseBody.data.availability).toBeDefined();
const availabilities = responseBody.data?.availability?.filter(
(availability: any[]) => availability.length
);
expect(availabilities?.length).toEqual(6);
expect(responseBody.data.schedule.availability?.[0]?.days).toEqual(
createdSchedule.availability?.[0]?.days
);
expect(responseBody.data.schedule.availability?.[0]?.startTime).toEqual(
`1970-01-01T${createdSchedule.availability?.[0]?.startTime}.000Z`
);
expect(responseBody.data.schedule.availability?.[0]?.endTime).toEqual(
`1970-01-01T${createdSchedule.availability?.[0]?.endTime}.000Z`
);
const availability = responseData.data.schedule.availability;
expect(availability).toBeDefined();
expect(availability?.length).toEqual(1);
expect(availability?.[0]?.days).toEqual(defaultAvailabilityDays);
expect(availability?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
expect(availability?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
createdSchedule.name = newScheduleName;
});
@@ -360,6 +188,10 @@ describe("Schedules Endpoints", () => {
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
try {
await scheduleRepositoryFixture.deleteById(createdSchedule.id);
} catch (e) {}
await app.close();
});
});
@@ -1,6 +1,10 @@
import { ResponseService } from "@/ee/schedules/services/response/response.service";
import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output";
import { DeleteScheduleOutput } from "@/ee/schedules/outputs/delete-schedule.output";
import { GetDefaultScheduleOutput } from "@/ee/schedules/outputs/get-default-schedule.output";
import { GetScheduleOutput } from "@/ee/schedules/outputs/get-schedule.output";
import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output";
import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output";
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
import { ForAtom } from "@/lib/atoms/decorators/for-atom.decorator";
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";
@@ -18,13 +22,11 @@ import {
Patch,
UseGuards,
} from "@nestjs/common";
import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
import { Throttle } from "@nestjs/throttler";
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import type { ScheduleWithAvailabilitiesForWeb } from "@calcom/platform-libraries";
import { updateScheduleHandler } from "@calcom/platform-libraries";
import type { UpdateScheduleOutputType } from "@calcom/platform-libraries";
import { ApiSuccessResponse, ScheduleResponse, UpdateScheduleInput } from "@calcom/platform-types";
import { ApiResponse } from "@calcom/platform-types";
import { UpdateScheduleInput } from "@calcom/platform-types";
import { CreateScheduleInput } from "../inputs/create-schedule.input";
@@ -33,21 +35,18 @@ import { CreateScheduleInput } from "../inputs/create-schedule.input";
version: "2",
})
@UseGuards(AccessTokenGuard, PermissionsGuard)
@DocsTags("Schedules")
export class SchedulesController {
constructor(
private readonly schedulesService: SchedulesService,
private readonly schedulesResponseService: ResponseService
) {}
constructor(private readonly schedulesService: SchedulesService) {}
@Post("/")
@Permissions([SCHEDULE_WRITE])
async createSchedule(
@GetUser() user: UserWithProfile,
@Body() bodySchedule: CreateScheduleInput,
@ForAtom() forAtom: boolean
): Promise<ApiSuccessResponse<ScheduleResponse | ScheduleWithAvailabilitiesForWeb>> {
@Body() bodySchedule: CreateScheduleInput
): Promise<CreateScheduleOutput> {
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
const scheduleFormatted = await this.schedulesResponseService.formatSchedule(forAtom, user, schedule);
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
return {
status: SUCCESS_STATUS,
@@ -57,13 +56,11 @@ export class SchedulesController {
@Get("/default")
@Permissions([SCHEDULE_READ])
async getDefaultSchedule(
@GetUser() user: UserWithProfile,
@ForAtom() forAtom: boolean
): Promise<ApiResponse<ScheduleResponse | ScheduleWithAvailabilitiesForWeb | null>> {
@ApiResponse({ status: 200, description: "Returns the default schedule", type: GetDefaultScheduleOutput })
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetDefaultScheduleOutput | null> {
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
const scheduleFormatted = schedule
? await this.schedulesResponseService.formatSchedule(forAtom, user, schedule)
? await this.schedulesService.formatScheduleForAtom(user, schedule)
: null;
return {
@@ -74,13 +71,13 @@ export class SchedulesController {
@Get("/:scheduleId")
@Permissions([SCHEDULE_READ])
@Throttle({ default: { limit: 10, ttl: 60000 } }) // allow 10 requests per minute (for :scheduleId)
async getSchedule(
@GetUser() user: UserWithProfile,
@Param("scheduleId") scheduleId: number,
@ForAtom() forAtom: boolean
): Promise<ApiSuccessResponse<ScheduleResponse | ScheduleWithAvailabilitiesForWeb>> {
@Param("scheduleId") scheduleId: number
): Promise<GetScheduleOutput> {
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
const scheduleFormatted = await this.schedulesResponseService.formatSchedule(forAtom, user, schedule);
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
return {
status: SUCCESS_STATUS,
@@ -90,12 +87,9 @@ export class SchedulesController {
@Get("/")
@Permissions([SCHEDULE_READ])
async getSchedules(
@GetUser() user: UserWithProfile,
@ForAtom() forAtom: boolean
): Promise<ApiSuccessResponse<ScheduleResponse[] | ScheduleWithAvailabilitiesForWeb[]>> {
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput> {
const schedules = await this.schedulesService.getUserSchedules(user.id);
const schedulesFormatted = await this.schedulesResponseService.formatSchedules(forAtom, user, schedules);
const schedulesFormatted = await this.schedulesService.formatSchedulesForAtom(user, schedules);
return {
status: SUCCESS_STATUS,
@@ -108,12 +102,14 @@ export class SchedulesController {
@Permissions([SCHEDULE_WRITE])
async updateSchedule(
@GetUser() user: UserWithProfile,
@Body() bodySchedule: UpdateScheduleInput
): Promise<ApiSuccessResponse<UpdateScheduleOutputType>> {
const updatedSchedule: UpdateScheduleOutputType = await updateScheduleHandler({
input: bodySchedule,
ctx: { user },
});
@Body() bodySchedule: UpdateScheduleInput,
@Param("scheduleId") scheduleId: string
): Promise<UpdateScheduleOutput> {
const updatedSchedule = await this.schedulesService.updateUserSchedule(
user,
Number(scheduleId),
bodySchedule
);
return {
status: SUCCESS_STATUS,
@@ -127,7 +123,7 @@ export class SchedulesController {
async deleteSchedule(
@GetUser("id") userId: number,
@Param("scheduleId") scheduleId: number
): Promise<ApiResponse> {
): Promise<DeleteScheduleOutput> {
await this.schedulesService.deleteUserSchedule(userId, scheduleId);
return {
@@ -16,6 +16,5 @@ export class CreateScheduleInput {
availabilities?: CreateAvailabilityInput[];
@IsBoolean()
@IsOptional()
isDefault = true;
isDefault!: boolean;
}
@@ -1,22 +0,0 @@
import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input";
import { Type } from "class-transformer";
import { IsBoolean, IsOptional, IsString, IsTimeZone, ValidateNested } from "class-validator";
export class UpdateScheduleInput {
@IsString()
@IsOptional()
name?: string;
@IsTimeZone()
@IsOptional()
timeZone?: string;
@ValidateNested({ each: true })
@Type(() => CreateAvailabilityInput)
@IsOptional()
availabilities?: CreateAvailabilityInput[];
@IsBoolean()
@IsOptional()
isDefault?: boolean;
}
@@ -0,0 +1,20 @@
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class CreateScheduleOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: ScheduleOutput,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => ScheduleOutput)
data!: ScheduleOutput;
}
@@ -0,0 +1,10 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class DeleteScheduleOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
}
@@ -0,0 +1,20 @@
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class GetDefaultScheduleOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: ScheduleOutput,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => ScheduleOutput)
data!: ScheduleOutput | null;
}
@@ -0,0 +1,20 @@
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class GetScheduleOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: ScheduleOutput,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => ScheduleOutput)
data!: ScheduleOutput;
}
@@ -0,0 +1,21 @@
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class GetSchedulesOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: ScheduleOutput,
})
@IsNotEmptyObject()
@ValidateNested({ each: true })
@Type(() => ScheduleOutput)
@IsArray()
data!: ScheduleOutput[];
}
@@ -0,0 +1,95 @@
import { Type } from "class-transformer";
import { IsBoolean, IsInt, IsOptional, IsString, ValidateNested, IsArray } from "class-validator";
class EventTypeModel {
@IsInt()
id!: number;
@IsOptional()
@IsString()
eventName?: string | null;
}
class AvailabilityModel {
@IsInt()
id!: number;
@IsOptional()
@IsInt()
userId?: number | null;
@IsOptional()
@IsInt()
scheduleId?: number | null;
@IsOptional()
@IsInt()
eventTypeId?: number | null;
@IsArray()
@IsInt({ each: true })
days!: number[];
@IsOptional()
@Type(() => Date)
@IsString() // Assuming date is serialized/deserialized appropriately
startTime?: Date;
@IsOptional()
@Type(() => Date)
@IsString() // Assuming date is serialized/deserialized appropriately
endTime?: Date;
@IsOptional()
@Type(() => Date)
@IsString() // Assuming date is serialized/deserialized appropriately
date?: Date | null;
}
class ScheduleModel {
@IsInt()
id!: number;
@IsInt()
userId!: number;
@IsString()
name!: string;
@IsOptional()
@IsString()
timeZone?: string | null;
@IsOptional()
@ValidateNested({ each: true })
@Type(() => EventTypeModel)
@IsArray()
eventType?: EventTypeModel[];
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AvailabilityModel)
@IsArray()
availability?: AvailabilityModel[];
}
export class UpdatedScheduleOutput {
@ValidateNested()
@Type(() => ScheduleModel)
schedule!: ScheduleModel;
@IsBoolean()
isDefault!: boolean;
@IsOptional()
@IsString()
timeZone?: string;
@IsOptional()
@IsInt()
prevDefaultId?: number | null;
@IsOptional()
@IsInt()
currentDefaultId?: number | null;
}
@@ -0,0 +1,105 @@
import { Type } from "class-transformer";
import { IsDate, IsOptional, IsArray, IsBoolean, IsInt, IsString, ValidateNested } from "class-validator";
class AvailabilityModel {
@IsInt()
id!: number;
@IsOptional()
@IsInt()
userId?: number | null;
@IsOptional()
@IsInt()
eventTypeId?: number | null;
@IsArray()
@IsInt({ each: true })
days!: number[];
@IsDate()
@Type(() => Date)
startTime!: Date;
@IsDate()
@Type(() => Date)
endTime!: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
date?: Date | null;
@IsOptional()
@IsInt()
scheduleId?: number | null;
}
class WorkingHours {
@IsArray()
@IsInt({ each: true })
days!: number[];
@IsInt()
startTime!: number;
@IsInt()
endTime!: number;
@IsOptional()
@IsInt()
userId?: number | null;
}
class TimeRange {
@IsOptional()
@IsInt()
userId?: number | null;
@IsDate()
start!: Date;
@IsDate()
end!: Date;
}
export class ScheduleOutput {
@IsInt()
id!: number;
@IsString()
name!: string;
@IsBoolean()
isManaged!: boolean;
@ValidateNested({ each: true })
@Type(() => WorkingHours)
workingHours!: WorkingHours[];
@ValidateNested({ each: true })
@Type(() => AvailabilityModel)
@IsArray()
schedule!: AvailabilityModel[];
availability!: TimeRange[][];
@IsString()
timeZone!: string;
@ValidateNested({ each: true })
@IsArray()
// note(Lauris) it should be
// dateOverrides!: { ranges: TimeRange[] }[];
// but docs aren't generating correctly it results in array of strings
dateOverrides!: unknown[];
@IsBoolean()
isDefault!: boolean;
@IsBoolean()
isLastSchedule!: boolean;
@IsBoolean()
readOnly!: boolean;
}
@@ -0,0 +1,20 @@
import { UpdatedScheduleOutput } from "@/ee/schedules/outputs/schedule-updated.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class UpdateScheduleOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: UpdatedScheduleOutput,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => UpdatedScheduleOutput)
data!: UpdatedScheduleOutput;
}
@@ -1,6 +1,5 @@
import { SchedulesController } from "@/ee/schedules/controllers/schedules.controller";
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
import { ResponseService } from "@/ee/schedules/services/response/response.service";
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
@@ -10,8 +9,8 @@ import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, AvailabilitiesModule, UsersModule, TokensModule],
providers: [SchedulesRepository, SchedulesService, ResponseService],
providers: [SchedulesRepository, SchedulesService],
controllers: [SchedulesController],
exports: [SchedulesService],
exports: [SchedulesService, SchedulesRepository],
})
export class SchedulesModule {}
@@ -1,9 +1,8 @@
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
import { UpdateScheduleInput } from "@/ee/schedules/inputs/update-schedule.input";
import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { BadRequestException, Injectable } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { Prisma } from "@prisma/client";
@Injectable()
@@ -78,51 +77,6 @@ export class SchedulesRepository {
return schedules;
}
async updateScheduleWithAvailabilities(scheduleId: number, schedule: UpdateScheduleInput) {
const existingSchedule = await this.dbRead.prisma.schedule.findUnique({
where: { id: scheduleId },
});
if (!existingSchedule) {
throw new BadRequestException(`Schedule with ID=${scheduleId} not found`);
}
const updatedScheduleData: Prisma.ScheduleUpdateInput = {
user: {
connect: {
id: existingSchedule.userId,
},
},
};
if (schedule.name) updatedScheduleData.name = schedule.name;
if (schedule.timeZone) updatedScheduleData.timeZone = schedule.timeZone;
if (schedule.availabilities && schedule.availabilities.length > 0) {
await this.dbWrite.prisma.availability.deleteMany({
where: { scheduleId },
});
updatedScheduleData.availability = {
createMany: {
data: schedule.availabilities.map((availability) => ({
...availability,
userId: existingSchedule.userId,
})),
},
};
}
const updatedSchedule = await this.dbWrite.prisma.schedule.update({
where: { id: scheduleId },
data: updatedScheduleData,
include: {
availability: true,
},
});
return updatedSchedule;
}
async deleteScheduleById(scheduleId: number) {
return this.dbWrite.prisma.schedule.delete({
where: {
@@ -1,68 +0,0 @@
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
import { Injectable } from "@nestjs/common";
import { User } from "@prisma/client";
import {
transformWorkingHoursForClient,
transformAvailabilityForClient,
transformDateOverridesForClient,
} from "@calcom/platform-libraries";
import type {
ScheduleWithAvailabilities,
ScheduleWithAvailabilitiesForWeb,
} from "@calcom/platform-libraries";
import { ScheduleResponse, schemaScheduleResponse } from "@calcom/platform-types";
@Injectable()
export class ResponseService {
constructor(private readonly schedulesRepository: SchedulesRepository) {}
async formatSchedule(
forAtom: boolean,
user: User,
schedule: ScheduleWithAvailabilities
): Promise<ScheduleResponse | ScheduleWithAvailabilitiesForWeb> {
if (forAtom) {
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
return this.transformScheduleForAtom(schedule, usersSchedulesCount, user);
}
return schemaScheduleResponse.parse(schedule);
}
async formatSchedules(
forAtom: boolean,
user: User,
schedules: ScheduleWithAvailabilities[]
): Promise<ScheduleResponse[] | ScheduleWithAvailabilitiesForWeb[]> {
if (forAtom) {
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
return Promise.all(
schedules.map((schedule) => this.transformScheduleForAtom(schedule, usersSchedulesCount, user))
);
}
return schedules.map((schedule) => schemaScheduleResponse.parse(schedule));
}
async transformScheduleForAtom(
schedule: ScheduleWithAvailabilities,
userSchedulesCount: number,
user: Pick<User, "id" | "defaultScheduleId" | "timeZone">
): Promise<ScheduleWithAvailabilitiesForWeb> {
const timeZone = schedule.timeZone || user.timeZone;
return {
id: schedule.id,
name: schedule.name,
isManaged: schedule.userId !== user.id,
workingHours: transformWorkingHoursForClient(schedule),
schedule: schedule.availability,
availability: transformAvailabilityForClient(schedule),
timeZone,
dateOverrides: transformDateOverridesForClient(schedule, timeZone),
isDefault: user.defaultScheduleId === schedule.id,
isLastSchedule: userSchedulesCount <= 1,
readOnly: schedule.userId !== user.id,
};
}
}
@@ -1,10 +1,20 @@
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
import { UpdateScheduleInput } from "@/ee/schedules/inputs/update-schedule.input";
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
import { AvailabilitiesService } from "@/modules/availabilities/availabilities.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { Schedule } from "@prisma/client";
import { User } from "@prisma/client";
import type { ScheduleWithAvailabilities } from "@calcom/platform-libraries";
import { updateScheduleHandler } from "@calcom/platform-libraries";
import {
transformWorkingHoursForClient,
transformAvailabilityForClient,
transformDateOverridesForClient,
} from "@calcom/platform-libraries";
import { UpdateScheduleInput } from "@calcom/platform-types";
@Injectable()
export class SchedulesService {
@@ -14,6 +24,16 @@ export class SchedulesService {
private readonly usersRepository: UsersRepository
) {}
async createUserDefaultSchedule(userId: number, timeZone: string) {
const schedule = {
isDefault: true,
name: "Default schedule",
timeZone,
};
return this.createUserSchedule(userId, schedule);
}
async createUserSchedule(userId: number, schedule: CreateScheduleInput) {
const availabilities = schedule.availabilities?.length
? schedule.availabilities
@@ -56,25 +76,31 @@ export class SchedulesService {
return this.schedulesRepository.getSchedulesByUserId(userId);
}
async updateUserSchedule(userId: number, scheduleId: number, schedule: UpdateScheduleInput) {
async updateUserSchedule(user: UserWithProfile, scheduleId: number, bodySchedule: UpdateScheduleInput) {
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
if (!existingSchedule) {
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
}
this.checkUserOwnsSchedule(userId, existingSchedule);
this.checkUserOwnsSchedule(user.id, existingSchedule);
const updatedSchedule = await this.schedulesRepository.updateScheduleWithAvailabilities(
scheduleId,
schedule
);
const schedule = await this.getUserSchedule(user.id, Number(scheduleId));
const scheduleFormatted = await this.formatScheduleForAtom(user, schedule);
if (schedule.isDefault) {
await this.usersRepository.setDefaultSchedule(userId, updatedSchedule.id);
if (!bodySchedule.schedule) {
// note(Lauris): When updating an availability in cal web app, lets say only its name, also
// the schedule is sent and then passed to the update handler. Notably, availability is passed too
// and they have same shape, so to match shapes I attach "scheduleFormatted.availability" to reflect
// schedule that would be passed by the web app. If we don't, then updating schedule name will erase
// schedule.
bodySchedule.schedule = scheduleFormatted.availability;
}
return updatedSchedule;
return updateScheduleHandler({
input: { scheduleId: Number(scheduleId), ...bodySchedule },
ctx: { user },
});
}
async deleteUserSchedule(userId: number, scheduleId: number) {
@@ -89,6 +115,44 @@ export class SchedulesService {
return this.schedulesRepository.deleteScheduleById(scheduleId);
}
async formatScheduleForAtom(user: User, schedule: ScheduleWithAvailabilities): Promise<ScheduleOutput> {
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
return this.transformScheduleForAtom(schedule, usersSchedulesCount, user);
}
async formatSchedulesForAtom(
user: User,
schedules: ScheduleWithAvailabilities[]
): Promise<ScheduleOutput[]> {
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
return Promise.all(
schedules.map((schedule) => this.transformScheduleForAtom(schedule, usersSchedulesCount, user))
);
}
async transformScheduleForAtom(
schedule: ScheduleWithAvailabilities,
userSchedulesCount: number,
user: Pick<User, "id" | "defaultScheduleId" | "timeZone">
): Promise<ScheduleOutput> {
const timeZone = schedule.timeZone || user.timeZone;
const defaultSchedule = await this.getUserScheduleDefault(user.id);
return {
id: schedule.id,
name: schedule.name,
isManaged: schedule.userId !== user.id,
workingHours: transformWorkingHoursForClient(schedule),
schedule: schedule.availability,
availability: transformAvailabilityForClient(schedule),
timeZone,
dateOverrides: transformDateOverridesForClient(schedule, timeZone),
isDefault: defaultSchedule?.id === schedule.id,
isLastSchedule: userSchedulesCount <= 1,
readOnly: schedule.userId !== user.id,
};
}
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">) {
if (userId !== schedule.userId) {
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
+3
View File
@@ -12,6 +12,9 @@ export type Environment = {
SENTRY_DSN: string;
LOG_LEVEL: keyof typeof logLevels;
REDIS_URL: string;
STRIPE_API_KEY: string;
STRIPE_WEBHOOK_SECRET: string;
WEB_APP_URL: string;
};
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
@@ -13,13 +13,17 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const statusCode = exception.getStatus();
const requestId = request.headers["X-Request-Id"];
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
exception,
body: request.body,
headers: request.headers,
url: request.url,
method: request.method,
requestId,
});
response.status(statusCode).json({
status: ERROR_STATUS,
timestamp: new Date().toISOString(),
@@ -33,12 +33,15 @@ export class PrismaExceptionFilter implements ExceptionFilter {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const requestId = request.headers["X-Request-Id"];
this.logger.error(`PrismaError: ${error.message}`, {
error,
body: request.body,
headers: request.headers,
url: request.url,
method: request.method,
requestId,
});
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
status: ERROR_STATUS,
@@ -14,6 +14,7 @@ export class SentryFilter extends BaseExceptionFilter {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const requestId = request.headers["X-Request-Id"];
this.logger.error(`Sentry Exception Filter: ${exception?.message}`, {
exception,
@@ -21,6 +22,7 @@ export class SentryFilter extends BaseExceptionFilter {
headers: request.headers,
url: request.url,
method: request.method,
requestId,
});
// capture if client has been init
@@ -41,12 +41,15 @@ export class TRPCExceptionFilter implements ExceptionFilter {
break;
}
const requestId = request.headers["X-Request-Id"];
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
exception,
body: request.body,
headers: request.headers,
url: request.url,
method: request.method,
requestId,
});
response.status(statusCode).json({
@@ -14,6 +14,7 @@ export class ZodExceptionFilter implements ExceptionFilter {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const requestId = request.headers["X-Request-Id"];
this.logger.error(`ZodError: ${error.message}`, {
error,
@@ -21,6 +22,7 @@ export class ZodExceptionFilter implements ExceptionFilter {
headers: request.headers,
url: request.url,
method: request.method,
requestId,
});
response.status(HttpStatus.BAD_REQUEST).json({
+6 -2
View File
@@ -16,6 +16,7 @@ import { loggerConfig } from "./lib/logger";
const run = async () => {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
logger: WinstonModule.createLogger(loggerConfig()),
bodyParser: false,
});
const logger = new Logger("App");
@@ -23,10 +24,11 @@ const run = async () => {
try {
bootstrap(app);
const port = app.get(ConfigService<AppConfig, true>).get("api.port", { infer: true });
generateSwagger(app);
void generateSwagger(app);
await app.listen(port);
logger.log(`Application started on port: ${port}`);
} catch (error) {
console.error(error);
logger.error("Application crashed", {
error,
});
@@ -48,7 +50,9 @@ async function generateSwagger(app: NestExpressApplication<Server>) {
}
fs.writeFileSync(outputFile, JSON.stringify(document, null, 2), { encoding: "utf8" });
SwaggerModule.setup("docs", app, document);
SwaggerModule.setup("docs", app, document, {
customCss: ".swagger-ui .topbar { display: none }",
});
logger.log(`Swagger documentation available in the "/docs" endpoint\n`);
}
@@ -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();
}
}

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