Compare commits

...
54 Commits
Author SHA1 Message Date
GitHub Actions be525a27df chore: release v5.0.14
Post release / release (push) Canceled after 0s
2025-03-10 22:44:00 +00:00
Calcom BotandLingo.dev df037b008f feat: update translations via @lingodotdev (#19927)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-10 21:12:15 +00:00
amritandUdit Takkar 8fb4df4b8c fix: duplicate value found in common.json error (#19693)
* fix: duplicate value found in common.json error

* Remove subteam invitation translation from locales

* verify_email_email_button to verify_email_button

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-03-10 19:19:32 +00:00
6758654796 feat: round robin reset interval for teams (#19441)
* add component to set interval

* fix variable namings

* fetch data for given interval

* migration

* fix type errors in test

* test correct filtering for interval

* fix start end end times to interval

* fix type error

* pass rrResetInterval to getOrderedListOfLuckyUsers

* fix imports

* add translations

* add missing translation

* code clean up

* fix type errors

* fix type error

* fix import

* add translation

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-03-10 17:34:47 +00:00
Alex van AndelandMorgan Vernay 1e86f1046b chore: Fix build failure due to trpc inference depth (#19884)
* chore: Fix build failure due to trpc inference depth

* Also bump platform libraries typescript version

* fix: e2e test using unassigned variable

* chore: bump libraries to 0.0.142

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-03-10 17:06:05 +00:00
Eunjae LeeandAlex van Andel d3775cd920 fix: update filter styles on org member list (#19918)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-03-10 17:01:37 +00:00
Benny JooandAnik Dhabal Babu 3ab053bf17 chore: migrate oauth APIs to app router (#19824)
* migrate api/auth/oauth/me

* migrate the rest

* fix

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-10 17:56:53 +01:00
Benny Joo 4a4bee7140 chore: Remove typeform app (#19911)
* remove typeform app

* update yarn.lock
2025-03-10 16:54:20 +00:00
Benny Joo 9ba85ea0b2 chore: missing pieces to migrated apis (#19915)
* fix

* refactor
2025-03-10 15:24:03 +00:00
avnish100 d1d1472f3e fix: removed rounded top corners on webhook form (#19905) 2025-03-10 14:54:43 +00:00
Benny Jooandsean-brydon 8d18b3da38 chore: migrate saml/oidc APIs to App Router (#19515)
* migrate oidc

* migrate saml apis

* remove comment

* add force-dynamic

* fix

* fix

* refactor

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-03-10 14:36:11 +00:00
Benny Joo 631c5f7c53 migrate webhookTriggers and workflows apis (#19875) 2025-03-10 14:35:11 +00:00
f2ca2bdc83 fix: add badge for disabled workflow actions and templates in trial mode (#19675)
* add badge for trial mode

* fix tooltip issue

* fix typo

* add inactive team plan badge

* change to one salesforce request

* clean up

* fix type error

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2025-03-10 15:34:29 +01:00
Rajiv Sahal bb36807689 feat: update platform onboarding docs (#17941)
* update images

* restructure getting started for platform

* add steps on setting up a platform account

* PR feedback

* fixup

* add setup page

* move over setup to a separate page

* update images
2025-03-10 20:02:24 +05:30
Keith Williams 7ed32d3c77 perf: Reduce imports by moving bookings zod out of utils (#19869) 2025-03-10 13:54:05 +00:00
Morgan 5d84179345 refactor: circular dependencies platform packages (#19903)
* refactor: circular dependencies platform packages

* fixup! refactor: circular dependencies platform packages

* fixup! fixup! refactor: circular dependencies platform packages

* fix: circular deps in BaseCalProvider
2025-03-10 13:25:01 +00:00
cb437231ad fix: platform libraries barrel file extract event-types (#19846)
* fix: event-types

* chore: added "+ group link" tip (#19809)

* added + link tip

* nit

---------

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>

* Added fix to solve the id TypeError (#19810)

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>

* Revert "fix: prevent background scroll on opening timezone select menu (#19789)" (#19814)

This reverts commit 1a96149ce3.

* fix: Calendar events attached to workflow emails are missing meeting link (#19790)

* fixup! fix: Calendar events attached to workflow emails are missing meeting link (#19790)

* chore: fix type errors

* fix: wrong import in e2e file

* fix: tsconfig api v2 for /event-types

* fix: import and refactor

* fixed remaning type issues

* undo extra changes

* bump platform libs

* Update event-types.controller.e2e-spec.ts

* fix: test with seats enabled

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Harshit Singh <73997189+harshit078@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-03-10 12:34:53 +00:00
Somay Chauhan d47a5be659 fix: add documentaion to blockUnconfirmedBookingsInBooker (#19894)
* fix: add documentaion to `blockUnconfirmedBookingsInBooker`

* fix documentation of ConfirmationPolicy.type

* Update confirmation-policy.input.ts
2025-03-10 09:31:49 -03:00
sean-brydon e89211782c fix: calcom trpc circle deps slots handlers (#19896)
* remove validator file and include inside handler

* fixes slots and handler circle dep

* fix type in tests
2025-03-10 11:29:51 +00:00
sean-brydon fd454061e7 fix profile circle dep (#19892) 2025-03-10 11:05:14 +00:00
Rajiv SahalandMorgan Vernay 277ce1c244 refactor: extract slots from libraries barrel file (#19895)
* refactor: extract slots fn from libraries barrel file

* bump platform libs

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-03-10 10:33:29 +00:00
luzpaz 1848cd55b1 fix: fix playwright typos (#19794)
Found via `codespell -q 3 -S "*.svg,./apps/web/public/static/locales,./packages/app-store/stripepayment/lib/currencyOptions.ts,./packages/lib/freeEmailDomainCheck/freeEmailDomains.ts" -L afterall,atleast,datea,fo,incase,ist,nam,noone,notin,optionel,perview,pris,reccuring`
2025-03-10 07:39:58 +00:00
Anik Dhabal Babu b0e3cc4dd2 chore: add logs for debugging (#19851) 2025-03-10 03:34:52 +00:00
Morgan b3e344d226 chore: decouple calcom/ui/Tooltip from atoms (#19798)
* chore: decouple calcom/ui from atoms #1

* recover input from main

* fix: vite config dialog
2025-03-10 01:43:01 +00:00
Suyash Srivastava 810f3b834c fix:small typo with email (#19879) 2025-03-10 01:22:13 +00:00
Hariom Balhara bf1eacfd9b Ignore faulty org (#19825) 2025-03-10 00:58:48 +00:00
c5c654c879 feat: no-show confirmation does not pop up for single attendee (#19806)
* feat: no-show marking  confirmation does not pop up for single attendee bookings

* fix: Add toggle for no-show status

* simplify logic

* feat: Centralize no-show handling logic in BookingListItem

* Update BookingListItem.tsx

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-09 17:06:03 +00:00
sean-brydonandBenny Joo d1ce04edc1 chore: move miscellaneous small api pages to app dir api (#19699)
* version and verify-booking-token

* username route

* api/me

* logo route

* render email in dev

* link route

* future

* nope and geo location

* referal link and daily

* intercom route and dynamic variables

* intercom route and dynamic variables

* scim 2.0 and helpscout route

* app credentials

* api/book events

* fix daily import path in tests

* fix buildLegacyRequest generation

* fix type errors

* migrate the /teams/ routes

* move cron jobs

* fix daily import path in tests

* fix buildLegacyRequest generation

* fix type errors

* migrate the /teams/ routes

* move cron jobs

* Revert "api/book events"

This reverts commit 607a32fb5b754cad090c2d0cbf64a68f990e220e.

* mock next server NextResponse to fix daily video

* add default responder to teams api

* fix search params

* uses nextUrl.searchParams instead of new url

* uses nextUrl.searchParams instead of new url

* remove outdated api config

* remove app dir version of inbound dynamic variables

* restore pages version of inbound variables

* fix missing code from stupid cursor

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-03-09 11:56:56 -04:00
Benny JooandAnik Dhabal Babu 1f1862e526 chore: migrate two-factor APIs to app router (#19826)
* wip

* remove comment

* clean up

* clean up

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-09 15:40:27 +00:00
Benny Joo 63dfc3376c chore: remove empty path from middleware matcher (#19860) 2025-03-09 15:38:04 +00:00
Adam BashaandKeith Williams 3166db9bcf chore: update robots.txt to disallow /settings and /my-account (#19867)
* chore: update robots.txt to disallow /settings and /my-account

* Update robots.txt

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-09 08:53:34 +00:00
Vijay f8af9dc17e fix: failing tests during DST transition (#19862)
* make the test dst transition proof

* fix other test similarly
2025-03-09 00:43:42 +00:00
Benny Joo ae659b5176 chore: migrate forgot-password, reset-password, auth/setup apis to App Router (#19512)
* chore: migrate /api/auth/changepw

* migrate forgot-password and reset-password

* remove unneeded export

* rename request to req for easier review

* migrate auth/setup

* remove setup

* refactor

* remove redundant code

* fix

* remove changepw api and unused ui
2025-03-07 14:43:36 -07:00
Eunjae Lee 33226f268d fix: date range on /bookings (#19785) 2025-03-07 21:54:49 +01:00
sean-brydonandAnik Dhabal Babu 7e2a0d1a1b fix: calcom UI circle dep (#19839)
* editor circle dep fix

* circle dep fix in select

* fix barrel export types

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-07 09:26:26 -07:00
a385bd4d5f feat: team ooo read-only for non-admin (#19700)
* feat team ooo read-only for non-admin

* Update apps/web/playwright/out-of-office.e2e.ts

Co-authored-by: Younes Henni <youneshenniwrites@gmail.com>

* updated error msgs with t

* nit

* nit

---------

Co-authored-by: Younes Henni <youneshenniwrites@gmail.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-07 16:19:30 +00:00
Anik Dhabal Babu 4b4bc28f1d fix: Calendar events attached to workflow emails are missing meeting link (#19790) 2025-03-07 14:41:51 +00:00
Anik Dhabal Babu 316fba908b Revert "fix: prevent background scroll on opening timezone select menu (#19789)" (#19814)
This reverts commit 1a96149ce3.
2025-03-07 14:39:09 +00:00
d40d282912 Added fix to solve the id TypeError (#19810)
Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-07 14:26:36 +00:00
e81df6fdeb chore: added "+ group link" tip (#19809)
* added + link tip

* nit

---------

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-07 14:23:51 +00:00
Hariom Balhara 76420411b9 fix: fixes the crash when empty users or hosts array is passed to _getDelegationCredentialsMapPerUser (#19834)
* fix crash when no users are there

* typo fix
2025-03-07 13:11:07 +00:00
Keith Williams 7ec1617dc2 chore: Update CODEOWNERS for package.json files (#19831) 2025-03-07 11:02:33 +00:00
Rajiv Sahal 870cadd81f fix: dry run improvements for v2 (#19196)
* add dry run param for v2 getSlots

* fixup

* revert changes

* dont reserve a slot for booking dry run

* accept dry run prop as a parameter instead of computing

* fix merge conflicts

* fixup

* dont reserve slots for booking dry run
2025-03-07 14:49:54 +05:30
amrit 04c581a25e docs: Update README (#19815) 2025-03-07 10:32:33 +05:30
Syed Ali Shahbaz 5ef131333d update SAML guide (#19778) 2025-03-07 09:00:53 +05:30
GitHub Actions c262220f0f chore: release v5.0.13
Post release / release (push) Canceled after 0s
2025-03-07 00:38:07 +00:00
Morgan b5e08ea80f fix: api v2 create event-type with delegation credentials (#19797) 2025-03-06 19:46:46 +02:00
Joe Au-Yeung 00ee1ef441 refactor: Salesforce - account query based on website (#19799)
* Refactor account lookup to include website prefixes

* Type fix
2025-03-06 21:53:49 +05:30
Rajiv Sahal 6cdc9c9f59 fix invalid access token bug (#19793) 2025-03-06 14:51:39 +00:00
Pranav Rajveerandamrit 56d1998f0f chore: rename virtual-queue to router-position (#19774)
Co-authored-by: amrit <iamamrit27@gmail.com>
2025-03-06 19:32:30 +05:30
Udit Takkar d59fdf21fd feat: display country code from IP (#19788)
* feat: display country code from IP

* chore: remove

* fix: unit test
2025-03-06 19:16:41 +05:30
suraj 1a96149ce3 fix: prevent background scroll on opening timezone select menu (#19789) 2025-03-06 13:29:50 +00:00
Eunjae Lee d2bc81758c feat: add booking id & booking link columns to /insights/routing (#19697)
* feat: add booking id & booking link columns to /insights/routing

* update style

* update i18n

* update i18n keys

* update host

* useCopy instead of raw navigator.clipboard
2025-03-06 10:24:50 +00:00
Alex van Andel 0565e23818 refactor: Remove intervalLimits from @calcom/lib and export directly (#19710)
* refactor: Remove intervalLimits from @calcom/lib and export directly

* Tackle other places that use parseBookingLimit/parseDurationLimit

* More type fixups that were hidden by previous fails

* Fixed up booking-limits file

* Remove server-only
2025-03-05 19:32:53 +00:00
387 changed files with 4113 additions and 7890 deletions
+1
View File
@@ -1,3 +1,4 @@
/**/package.json @calcom/Foundation
/apps/api/**/* @calcom/Platform @calcom/Foundation
/deploy/**/* @calcom/Foundation
/infra/**/* @calcom/Foundation
View File
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+14 -13
View File
@@ -172,24 +172,25 @@ yarn dx
```
#### Development tip
1. Add `export NODE_OPTIONS=“--max-old-space-size=16384”` to your shell script to increase the memory limit for the node process. Alternatively, you can run this in your terminal before running the app. Replace 16384 with the amount of RAM you want to allocate to the node process.
Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
Where {level} can be one of the following:
2. Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
Where {level} can be one of the following:
`0` for silly \
`1` for trace \
`2` for debug \
`3` for info \
`4` for warn \
`5` for error \
`6` for fatal
`0` for silly \
`1` for trace \
`2` for debug \
`3` for info \
`4` for warn \
`5` for error \
`6` for fatal
When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works:
When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works:
The logger will include all logs that are at the specified level or higher. For example: \
The logger will include all logs that are at the specified level or higher. For example: \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \
```sh
echo 'NEXT_PUBLIC_LOGGER_LEVEL=3' >> .env
+2 -1
View File
@@ -8,7 +8,8 @@ import {
_TeamModel,
_UserModel,
} from "@calcom/prisma/zod";
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";
import { iso8601 } from "@calcom/prisma/zod-utils";
import { extendedBookingCreateBody } from "@calcom/prisma/zod/custom/booking";
import { schemaQueryUserId } from "./shared/queryUserId";
+2 -2
View File
@@ -32,7 +32,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.127",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.142",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -101,7 +101,7 @@
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.0",
"typescript": "^4.9.4"
"typescript": "^5.8.2"
},
"prisma": {
"schema": "../../../packages/prisma/schema.prisma"
@@ -19,7 +19,7 @@ import { NextApiRequest } from "next/types";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries";
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
import {
CancelBookingInput,
CancelBookingInput_2024_08_13,
@@ -442,7 +442,8 @@ export class InputBookingsService_2024_08_13 {
return keyData?.userId;
} else {
// Access Token
return this.oAuthFlowService.getOwnerId(bearerToken);
const ownerId = await this.oAuthFlowService.getOwnerId(bearerToken);
return ownerId;
}
}
} catch (err) {
@@ -208,7 +208,7 @@ export class ConnectedCalendar {
class DestinationCalendar {
@IsInt()
@ApiProperty()
id!: number;
id!: number | string;
@IsString()
@ApiProperty()
@@ -33,10 +33,10 @@ import {
} from "@calcom/platform-constants";
import {
EventTypesByViewer,
EventTypesPublic,
eventTypeBookingFields,
eventTypeLocations,
} from "@calcom/platform-libraries";
EventTypesPublic,
} from "@calcom/platform-libraries/event-types";
import { ApiSuccessResponse } from "@calcom/platform-types";
describe("Event types Endpoints", () => {
@@ -5,7 +5,7 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { getEventTypeById } from "@calcom/platform-libraries";
import { getEventTypeById } from "@calcom/platform-libraries/event-types";
import type { PrismaClient } from "@calcom/prisma";
@Injectable()
@@ -1,6 +1,8 @@
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_04_15/constants/constants";
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/editable";
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
@@ -16,7 +18,7 @@ import {
EventTypesPublic,
getEventTypesPublic,
systemBeforeFieldEmail,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/event-types";
import { EventType } from "@calcom/prisma/client";
@Injectable()
@@ -68,6 +70,7 @@ export class EventTypesService_2024_04_15 {
organization: { isOrgAdmin },
profile: { id: profileId },
metadata: user.metadata,
email: user.email,
};
}
@@ -134,7 +137,7 @@ export class EventTypesService_2024_04_15 {
!bookingFields.find((field) => field.type === "email") &&
!bookingFields.find((field) => field.type === "phone")
) {
bookingFields.push(systemBeforeFieldEmail);
bookingFields.push({ ...systemBeforeFieldEmail, type: BaseField.email, editable: Editable.system });
}
await updateEventType({
@@ -167,7 +170,15 @@ export class EventTypesService_2024_04_15 {
async getUserToUpdateEvent(user: UserWithProfile) {
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
return { ...user, profile: { id: profileId }, selectedCalendars };
const eventTypeSelectedCalendars =
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
return {
...user,
locale: user.locale ?? "en",
profile: { id: profileId },
userLevelSelectedCalendars: selectedCalendars,
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
};
}
async deleteEventType(eventTypeId: number, userId: number) {
@@ -1845,7 +1845,6 @@ describe("Event types Endpoints", () => {
await teamRepositoryFixture.delete(organization.id);
try {
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
} catch (e) {
// Event type might have been deleted by the test
}
@@ -8,9 +8,13 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { createEventType, updateEventType } from "@calcom/platform-libraries";
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
import { dynamicEvent } from "@calcom/platform-libraries";
import {
createEventType,
updateEventType,
getEventTypesPublic,
EventTypesPublic,
} from "@calcom/platform-libraries/event-types";
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
import { EventType } from "@calcom/prisma/client";
@@ -112,8 +116,11 @@ export class EventTypesService_2024_06_14 {
: false;
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
const eventTypeSelectedCalendars =
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
return {
id: user.id,
locale: user.locale ?? "en",
role: user.role,
username: user.username,
organizationId: user.organizationId,
@@ -121,6 +128,9 @@ export class EventTypesService_2024_06_14 {
profile: { id: profileId },
metadata: user.metadata,
selectedCalendars,
email: user.email,
userLevelSelectedCalendars: selectedCalendars,
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
};
}
@@ -204,7 +214,11 @@ export class EventTypesService_2024_06_14 {
return defaultEventTypes;
}
async updateEventType(eventTypeId: number, body: InputEventTransformed_2024_06_14, user: UserWithProfile) {
async updateEventType(
eventTypeId: number,
body: Partial<InputEventTransformed_2024_06_14>,
user: UserWithProfile
) {
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
@@ -230,7 +244,7 @@ export class EventTypesService_2024_06_14 {
};
}
async checkCanUpdateEventType(userId: number, eventTypeId: number, scheduleId: number | undefined) {
async checkCanUpdateEventType(userId: number, eventTypeId: number, scheduleId: number | undefined | null) {
const existingEventType = await this.getUserEventType(userId, eventTypeId);
if (!existingEventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
@@ -242,7 +256,15 @@ export class EventTypesService_2024_06_14 {
async getUserToUpdateEvent(user: UserWithProfile) {
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
return { ...user, profile: { id: profileId }, selectedCalendars };
const eventTypeSelectedCalendars =
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
return {
...user,
locale: user.locale ?? "en",
profile: { id: profileId },
userLevelSelectedCalendars: selectedCalendars,
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
};
}
async deleteEventType(eventTypeId: number, userId: number) {
@@ -25,13 +25,17 @@ import {
transformSeatsApiToInternal,
SystemField,
CustomField,
} from "@calcom/platform-libraries";
InternalLocation,
InternalLocationSchema,
} from "@calcom/platform-libraries/event-types";
import {
CreateEventTypeInput_2024_06_14,
DestinationCalendar_2024_06_14,
InputEventTransformed_2024_06_14,
OutputUnknownLocation_2024_06_14,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
@@ -58,7 +62,7 @@ export class InputEventTypesService_2024_06_14 {
const transformedBody = this.transformInputCreateEventType(inputEventType);
await this.validateEventTypeInputs({
seatsPerTimeSlot: transformedBody.seatsPerTimeSlot,
seatsPerTimeSlot: transformedBody?.seatsPerTimeSlot || null,
locations: transformedBody.locations,
requiresConfirmation: transformedBody.requiresConfirmation,
eventName: transformedBody.eventName,
@@ -272,7 +276,12 @@ export class InputEventTypesService_2024_06_14 {
}
transformInputBookerLayouts(inputBookerLayouts: CreateEventTypeInput_2024_06_14["bookerLayouts"]) {
return transformBookerLayoutsApiToInternal(inputBookerLayouts);
const layouts = transformBookerLayoutsApiToInternal(inputBookerLayouts);
if (!layouts) return undefined;
return {
defaultLayout: layouts.defaultLayout as unknown as BookerLayouts,
enabledLayouts: layouts.enabledLayouts as unknown as BookerLayouts[],
};
}
transformInputConfirmationPolicy(
@@ -303,23 +312,22 @@ export class InputEventTypesService_2024_06_14 {
eventName,
}: ValidationContext) {
let seatsPerTimeSlotDb: number | null = null;
let locationsDb: ReturnType<typeof this.transformInputLocations> = [];
let locationsDb: ReturnType<typeof this.transformLocations> = [];
let requiresConfirmationDb = false;
if (eventTypeId != null) {
const eventTypeDb = await this.eventTypesRepository.getEventTypeWithSeats(eventTypeId);
seatsPerTimeSlotDb = eventTypeDb?.seatsPerTimeSlot ?? null;
locationsDb = this.outputEventTypesService.transformLocations(eventTypeDb?.locations) ?? [];
locationsDb = this.transformLocations(eventTypeDb?.locations) ?? [];
requiresConfirmationDb = eventTypeDb?.requiresConfirmation ?? false;
}
const seatsPerTimeSlotFinal = seatsPerTimeSlot !== undefined ? seatsPerTimeSlot : seatsPerTimeSlotDb;
const seatsEnabledFinal = seatsPerTimeSlotFinal != null && seatsPerTimeSlotFinal > 0;
const seatsPerTimeSlotFinal = !!seatsPerTimeSlot ? seatsPerTimeSlot : seatsPerTimeSlotDb;
const seatsEnabledFinal = !!seatsPerTimeSlotFinal && seatsPerTimeSlotFinal > 0;
const locationsFinal = locations !== undefined ? locations : locationsDb;
const requiresConfirmationFinal =
requiresConfirmation !== undefined ? requiresConfirmation : requiresConfirmationDb;
this.validateSeatsSingleLocationRule(seatsEnabledFinal, locationsFinal);
this.validateSeatsRequiresConfirmationFalseRule(seatsEnabledFinal, requiresConfirmationFinal);
this.validateMultipleLocationsSeatsDisabledRule(locationsFinal, seatsEnabledFinal);
@@ -331,7 +339,7 @@ export class InputEventTypesService_2024_06_14 {
}
validateSeatsSingleLocationRule(
seatsEnabled: boolean,
locations: ReturnType<typeof this.transformInputLocations>
locations: ReturnType<typeof this.transformLocations>
) {
if (seatsEnabled && locations.length > 1) {
throw new BadRequestException(
@@ -339,6 +347,22 @@ export class InputEventTypesService_2024_06_14 {
);
}
}
transformLocations(locations: any) {
if (!locations) return [];
const knownLocations: InternalLocation[] = [];
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
for (const location of locations) {
const result = InternalLocationSchema.safeParse(location);
if (result.success) {
knownLocations.push(result.data);
} else {
unknownLocations.push({ type: "unknown", location: JSON.stringify(location) });
}
}
return [...knownLocations];
}
validateSeatsRequiresConfirmationFalseRule(seatsEnabled: boolean, requiresConfirmation: boolean) {
if (seatsEnabled && requiresConfirmation) {
@@ -349,7 +373,7 @@ export class InputEventTypesService_2024_06_14 {
}
validateMultipleLocationsSeatsDisabledRule(
locations: ReturnType<typeof this.transformInputLocations>,
locations: ReturnType<typeof this.transformLocations>,
seatsEnabled: boolean
) {
if (locations.length > 1 && seatsEnabled) {
@@ -2,15 +2,18 @@ import { Injectable } from "@nestjs/common";
import type { EventType, User, Schedule, DestinationCalendar } from "@prisma/client";
import {
EventTypeMetaDataSchema,
userMetadata,
parseBookingLimit,
parseRecurringEvent,
getBookingFieldsWithSystemFields,
} from "@calcom/platform-libraries";
import {
EventTypeMetaDataSchema,
transformLocationsInternalToApi,
transformBookingFieldsInternalToApi,
parseRecurringEvent,
InternalLocationSchema,
SystemField,
CustomField,
parseBookingLimit,
transformIntervalLimitsInternalToApi,
transformFutureBookingLimitsInternalToApi,
transformRecurrenceInternalToApi,
@@ -21,8 +24,7 @@ import {
transformSeatsInternalToApi,
InternalLocation,
BookingFieldSchema,
getBookingFieldsWithSystemFields,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/event-types";
import {
TransformFutureBookingsLimitSchema_2024_06_14,
BookerLayoutsTransformedSchema,
@@ -198,13 +200,13 @@ export class OutputEventTypesService_2024_06_14 {
};
}
transformLocations(locations: any) {
if (!locations) return [];
transformLocations(locationDb: any) {
if (!locationDb) return [];
const knownLocations: InternalLocation[] = [];
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
for (const location of locations) {
for (const location of locationDb) {
const result = InternalLocationSchema.safeParse(location);
if (result.success) {
knownLocations.push(result.data);
@@ -315,6 +317,7 @@ export class OutputEventTypesService_2024_06_14 {
transformEventTypeColor(eventTypeColor: any) {
if (!eventTypeColor) return undefined;
const parsedeventTypeColor = parseEventTypeColor(eventTypeColor);
if (!parsedeventTypeColor) return undefined;
return transformEventTypeColorsInternalToApi(parsedeventTypeColor);
}
@@ -23,8 +23,8 @@ import {
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries";
import { ConnectedApps } from "@calcom/platform-libraries";
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries/event-types";
import { ApiResponse } from "@calcom/platform-types";
/*
@@ -81,7 +81,7 @@ export class AtomsController {
): Promise<ApiResponse<unknown>> {
const { teamId } = queryParams;
const app = await this.eventTypesService.getEventTypesAppIntegration(appSlug, user.id, user.name, teamId);
const app = await this.eventTypesService.getEventTypesAppIntegration(appSlug, user, teamId);
return {
status: SUCCESS_STATUS,
@@ -124,7 +124,11 @@ export class AtomsController {
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@Body() body: UpdateEventTypeReturn
): Promise<ApiResponse<UpdateEventTypeReturn>> {
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
const eventType = await this.eventTypesService.updateEventType(
eventTypeId,
{ ...body, id: eventTypeId },
user
);
return {
status: SUCCESS_STATUS,
data: eventType,
@@ -140,7 +144,12 @@ export class AtomsController {
@Param("teamId", ParseIntPipe) teamId: number,
@Body() body: UpdateEventTypeReturn
): Promise<ApiResponse<UpdateEventTypeReturn>> {
const eventType = await this.eventTypesService.updateTeamEventType(eventTypeId, body, user, teamId);
const eventType = await this.eventTypesService.updateTeamEventType(
eventTypeId,
{ ...body, id: eventTypeId },
user,
teamId
);
return {
status: SUCCESS_STATUS,
data: eventType,
@@ -10,17 +10,11 @@ import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
import {
updateEventType,
TUpdateEventTypeInputSchema,
systemBeforeFieldEmail,
getEventTypeById,
getEnabledAppsFromCredentials,
getAppFromSlug,
MembershipRole,
EventTypeMetaDataSchema,
getClientSecretFromPayment,
getBulkEventTypes,
bulkUpdateEventsToDefaultLocation,
enrichUserWithDelegationConferencingCredentialsWithoutOrgId,
} from "@calcom/platform-libraries";
import type {
App,
@@ -31,6 +25,17 @@ import type {
TDependencyData,
CredentialPayload,
} from "@calcom/platform-libraries";
import {
getBulkEventTypes,
getEventTypeById,
bulkUpdateEventsToDefaultLocation,
} from "@calcom/platform-libraries/event-types";
import {
updateEventType,
TUpdateEventTypeInputSchema,
systemBeforeFieldEmail,
EventTypeMetaDataSchema,
} from "@calcom/platform-libraries/event-types";
import { PrismaClient } from "@calcom/prisma";
type EnabledAppType = App & {
@@ -104,7 +109,7 @@ export class EventTypesAtomService {
}
const eventType = await updateEventType({
input: { id: eventTypeId, ...body, bookingFields },
input: { ...body, id: eventTypeId, bookingFields },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -117,7 +122,7 @@ export class EventTypesAtomService {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
return eventType.eventType;
return eventType;
}
async updateEventType(eventTypeId: number, body: TUpdateEventTypeInputSchema, user: UserWithProfile) {
@@ -134,7 +139,7 @@ export class EventTypesAtomService {
}
const eventType = await updateEventType({
input: { id: eventTypeId, ...body, bookingFields },
input: { ...body, id: eventTypeId, bookingFields },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -147,10 +152,15 @@ export class EventTypesAtomService {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
return eventType.eventType;
return eventType;
}
async checkCanUpdateTeamEventType(userId: number, eventTypeId: number, teamId: number, scheduleId: number) {
async checkCanUpdateTeamEventType(
userId: number,
eventTypeId: number,
teamId: number,
scheduleId: number | null | undefined
) {
await this.checkTeamOwnsEventType(userId, eventTypeId, teamId);
await this.teamEventTypeService.validateEventTypeExists(teamId, eventTypeId);
await this.eventTypeService.checkUserOwnsSchedule(userId, scheduleId);
@@ -179,11 +189,11 @@ export class EventTypesAtomService {
}
}
async getEventTypesAppIntegration(slug: string, userId: number, userName: string | null, teamId?: number) {
let credentials = await this.credentialsRepository.getAllUserCredentialsById(userId);
async getEventTypesAppIntegration(slug: string, user: UserWithProfile, teamId?: number) {
let credentials = await this.credentialsRepository.getAllUserCredentialsById(user.id);
let userTeams: TeamQuery[] = [];
if (teamId) {
const teamsQuery = await this.atomsRepository.getUserTeams(userId);
const teamsQuery = await this.atomsRepository.getUserTeams(user.id);
// If a team is a part of an org then include those apps
// Don't want to iterate over these parent teams
const filteredTeams: TeamQuery[] = [];
@@ -211,8 +221,17 @@ export class EventTypesAtomService {
credentials = credentials.concat(teamAppCredentials);
}
}
//TODO: enrich credentials for DelegationCredential
const enabledApps = await getEnabledAppsFromCredentials(credentials, {
// We only add delegationCredentials if the request for location options is for a user because DelegationCredential Credential is applicable to Users only.
const { credentials: allCredentials } = await enrichUserWithDelegationConferencingCredentialsWithoutOrgId(
{
user: {
...user,
credentials,
},
}
);
credentials = allCredentials;
const enabledApps = await getEnabledAppsFromCredentials(allCredentials, {
where: { slug },
});
const apps = await Promise.all(
@@ -265,7 +284,7 @@ export class EventTypesAtomService {
});
}
const credentialOwner: CredentialOwner = {
name: userName,
name: user.name,
teamId,
};
return {
@@ -4,7 +4,7 @@ import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { transformTeamLocationsApiToInternal } from "@calcom/platform-libraries";
import { transformTeamLocationsApiToInternal } from "@calcom/platform-libraries/event-types";
import {
CreateTeamEventTypeInput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
@@ -12,6 +12,13 @@ import {
} from "@calcom/platform-types";
import { SchedulingType } from "@calcom/prisma/client";
export type TransformedCreateTeamEventTypeInput = Awaited<
ReturnType<InstanceType<typeof InputOrganizationsEventTypesService>["transformInputCreateTeamEventType"]>
>;
export type TransformedUpdateTeamEventTypeInput = Awaited<
ReturnType<InstanceType<typeof InputOrganizationsEventTypesService>["transformInputUpdateTeamEventType"]>
>;
@Injectable()
export class InputOrganizationsEventTypesService {
constructor(
@@ -1,5 +1,9 @@
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
import {
TransformedCreateTeamEventTypeInput,
TransformedUpdateTeamEventTypeInput,
} from "@/modules/organizations/event-types/services/input.service";
import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
@@ -7,8 +11,8 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, Logger } from "@nestjs/common";
import { createEventType } from "@calcom/platform-libraries";
import { InputTeamEventTransformed_2024_06_14 } from "@calcom/platform-types";
// eslint-disable-next-line @typescript-eslint/no-var-requires
import { createEventType } from "@calcom/platform-libraries/event-types";
@Injectable()
export class OrganizationsEventTypesService {
@@ -26,14 +30,16 @@ export class OrganizationsEventTypesService {
user: UserWithProfile,
teamId: number,
orgId: number,
body: InputTeamEventTransformed_2024_06_14
body: TransformedCreateTeamEventTypeInput
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
const eventTypeUser = await this.getUserToCreateTeamEvent(user, orgId);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { hosts, children, destinationCalendar, ...rest } = body;
const { eventType: eventTypeCreated } = await createEventType({
input: { teamId: teamId, ...rest },
input: {
teamId: teamId,
...rest,
},
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -57,6 +63,7 @@ export class OrganizationsEventTypesService {
organization: { isOrgAdmin },
profile: { id: profileId || null },
metadata: user.metadata,
email: user.email,
};
}
@@ -87,7 +94,7 @@ export class OrganizationsEventTypesService {
async updateTeamEventType(
eventTypeId: number,
teamId: number,
body: InputTeamEventTransformed_2024_06_14,
body: TransformedUpdateTeamEventTypeInput,
user: UserWithProfile
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user);
@@ -37,7 +37,7 @@ import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries";
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries/event-types";
import { SkipTakePagination } from "@calcom/platform-types";
@Controller({
@@ -62,6 +62,15 @@ export class SelectedCalendarsRepository {
});
}
getUserEventTypeSelectedCalendar(userId: number) {
return this.dbRead.prisma.selectedCalendar.findMany({
where: {
userId,
eventTypeId: { not: null },
},
});
}
async addUserSelectedCalendar(
userId: number,
integration: string,
@@ -1,4 +1,5 @@
import { SlotsOutputService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
import type { RangeSlots, TimeSlots } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
import { SlotsService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots.service";
import { Query, Body, Controller, Get, Delete, Post, Req, Res } from "@nestjs/common";
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
@@ -12,8 +13,7 @@ import {
VERSION_2024_06_11,
VERSION_2024_08_13,
} from "@calcom/platform-constants";
import { getAvailableSlots } from "@calcom/platform-libraries";
import type { AvailableSlotsType } from "@calcom/platform-libraries";
import { getAvailableSlots } from "@calcom/platform-libraries/slots";
import { RemoveSelectedSlotInput_2024_04_15, ReserveSlotInput_2024_04_15 } from "@calcom/platform-types";
import { ApiResponse, GetAvailableSlotsInput_2024_04_15 } from "@calcom/platform-types";
@@ -156,7 +156,7 @@ export class SlotsController_2024_04_15 {
async getAvailableSlots(
@Query() query: GetAvailableSlotsInput_2024_04_15,
@Req() req: ExpressRequest
): Promise<ApiResponse<AvailableSlotsType>> {
): Promise<ApiResponse<{ slots: TimeSlots["slots"] | RangeSlots["slots"] }>> {
const isTeamEvent = await this.slotsService.checkIfIsTeamEvent(query.eventTypeId);
const availableSlots = await getAvailableSlots({
input: {
@@ -7,7 +7,7 @@ import { SlotFormat } from "@calcom/platform-enums";
export type TimeSlots = {
slots: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>;
};
type RangeSlots = {
export type RangeSlots = {
slots: Record<string, { startTime: string; endTime: string; attendees?: number; bookingUid?: string }[]>;
};
@@ -1,10 +1,8 @@
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
import { SlotsRepository_2024_04_15 } from "@/modules/slots/slots-2024-04-15/slots.repository";
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
import { DateTime } from "luxon";
import { Injectable, NotFoundException } from "@nestjs/common";
import { v4 as uuid } from "uuid";
import { SlotFormat } from "@calcom/platform-enums";
import { ReserveSlotInput_2024_04_15 } from "@calcom/platform-types";
@Injectable()
@@ -33,7 +31,7 @@ export class SlotsService_2024_04_15 {
}
}
if (eventType && shouldReserveSlot) {
if (eventType && shouldReserveSlot && !input._isDryRun) {
await Promise.all(
eventType.users.map((user) =>
this.slotsRepo.upsertSelectedSlot(user.id, input, uid, eventType.seatsPerTimeSlot !== null)
@@ -2,7 +2,7 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_20
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { OrganizationsUsersRepository } from "@/modules/organizations/users/index/organizations-users.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { DateTime } from "luxon";
import { dynamicEvent } from "@calcom/platform-libraries";
@@ -88,6 +88,11 @@ export class SlotsInputService_2024_09_04 {
dateTime = dateTime.set({ hour: 23, minute: 59, second: 59 });
}
return dateTime.toISO();
const ISOEndTime = dateTime.toISO();
if (ISOEndTime === null) {
throw new BadRequestException("Invalid end date");
}
return ISOEndTime;
}
}
@@ -17,7 +17,7 @@ import {
import { DateTime } from "luxon";
import { z } from "zod";
import { getAvailableSlots } from "@calcom/platform-libraries";
import { getAvailableSlots } from "@calcom/platform-libraries/slots";
import { GetSlotsInput_2024_09_04, ReserveSlotInput_2024_09_04 } from "@calcom/platform-types";
import { EventType } from "@calcom/prisma/client";
@@ -1,5 +1,9 @@
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import {
TransformedCreateTeamEventTypeInput,
TransformedUpdateTeamEventTypeInput,
} from "@/modules/organizations/event-types/services/input.service";
import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
@@ -7,8 +11,7 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException, Logger } from "@nestjs/common";
import { createEventType, updateEventType } from "@calcom/platform-libraries";
import { InputTeamEventTransformed_2024_06_14 } from "@calcom/platform-types";
import { createEventType, updateEventType } from "@calcom/platform-libraries/event-types";
@Injectable()
export class TeamsEventTypesService {
@@ -25,7 +28,7 @@ export class TeamsEventTypesService {
async createTeamEventType(
user: UserWithProfile,
teamId: number,
body: InputTeamEventTransformed_2024_06_14
body: TransformedCreateTeamEventTypeInput
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
const eventTypeUser = await this.getUserToCreateTeamEvent(user);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -62,6 +65,7 @@ export class TeamsEventTypesService {
organization: { id: null, isOrgAdmin: false, metadata: {}, requestedSlug: null },
profile: { id: profileId || null },
metadata: user.metadata,
email: user.email,
};
}
@@ -100,14 +104,16 @@ export class TeamsEventTypesService {
async updateTeamEventType(
eventTypeId: number,
teamId: number,
body: InputTeamEventTransformed_2024_06_14,
body: TransformedUpdateTeamEventTypeInput,
user: UserWithProfile
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
await this.validateEventTypeExists(teamId, eventTypeId);
const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user);
await updateEventType({
input: { id: eventTypeId, ...body },
input: {
id: eventTypeId,
...body,
},
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -31,7 +31,7 @@ import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries";
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries/event-types";
import { SkipTakePagination } from "@calcom/platform-types";
@Controller({
+1 -1
View File
@@ -15999,7 +15999,7 @@
"type": "object",
"properties": {
"id": {
"type": "number"
"type": "object"
},
"integration": {
"type": "string"
+3 -1
View File
@@ -18,7 +18,9 @@
"@calcom/platform-constants": ["../../../packages/platform/constants/index.ts"],
"@calcom/platform-types": ["../../../packages/platform/types/index.ts"],
"@calcom/platform-utils": ["../../../packages/platform/utils/index.ts"],
"@calcom/platform-enums": ["../../../packages/platform/enums/index.ts"]
"@calcom/platform-enums": ["../../../packages/platform/enums/index.ts"],
"@calcom/platform-libraries/event-types": ["../../../packages/platform/libraries/event-types.ts"],
"@calcom/platform-libraries/slots": ["../../../packages/platform/libraries/slots.ts"]
},
"incremental": true,
"skipLibCheck": true,
@@ -4,7 +4,7 @@ import VerifyEmailPage from "~/auth/verify-email-view";
export const generateMetadata = async () => {
return await _generateMetadata(
(t) => t("verify_email"),
(t) => t("verify_email_button"),
() => ""
);
};
@@ -1,22 +1,23 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { passwordResetRequest } from "@calcom/features/auth/lib/passwordResetRequest";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { emailSchema } from "@calcom/lib/emailSchema";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import prisma from "@calcom/prisma";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const email = emailSchema.transform((val) => val.toLowerCase()).safeParse(req.body?.email);
async function handler(req: NextRequest) {
const email = emailSchema.transform((val) => val.toLowerCase()).safeParse((await req.json())?.email);
if (!email.success) {
return res.status(400).json({ message: "email is required" });
return NextResponse.json({ message: "email is required" }, { status: 400 });
}
// fallback to email if ip is not present
let ip = (req.headers["x-real-ip"] as string) ?? email.data;
let ip = (req.headers.get("x-real-ip") as string) ?? email.data;
const forwardedFor = req.headers["x-forwarded-for"] as string;
const forwardedFor = req.headers.get("x-forwarded-for") as string;
if (!ip && forwardedFor) {
ip = forwardedFor?.split(",").at(0) ?? email.data;
}
@@ -34,15 +35,15 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
select: { name: true, email: true, locale: true },
});
// Don't leak info about whether the user exists
if (!user) return res.status(201).json({ message: "password_reset_email_sent" });
if (!user) return NextResponse.json({ message: "password_reset_email_sent" }, { status: 201 });
await passwordResetRequest(user);
return res.status(201).json({ message: "password_reset_email_sent" });
return NextResponse.json({ message: "password_reset_email_sent" }, { status: 201 });
} catch (reason) {
console.error(reason);
return res.status(500).json({ message: "Unable to create password reset request" });
return NextResponse.json({ message: "Unable to create password reset request" }, { status: 500 });
}
}
export default defaultHandler({
POST: Promise.resolve({ default: handler }),
});
const postHandler = defaultResponderForAppDir(handler);
export { postHandler as POST };
+18
View File
@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import isAuthorized from "@calcom/features/auth/lib/oAuthAuthorization";
async function handler(req: NextRequest) {
const requiredScopes = ["READ_PROFILE"];
const token = req.headers.get("authorization")?.split(" ")[1] || "";
const account = await isAuthorized(token, requiredScopes);
if (!account) {
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
return NextResponse.json({ username: account.name }, { status: 201 });
}
export { handler as GET, handler as POST };
@@ -1,23 +1,16 @@
import jwt from "jsonwebtoken";
import type { NextApiRequest, NextApiResponse } from "next";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import prisma from "@calcom/prisma";
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
import type { OAuthTokenPayload } from "@calcom/types/oauth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
}
const refreshToken = req.headers.authorization?.split(" ")[1] || "";
const { client_id, client_secret, grant_type } = req.body;
export async function POST(req: NextRequest) {
const { client_id, client_secret, grant_type } = await req.json();
if (grant_type !== "refresh_token") {
res.status(400).json({ message: "grant type invalid" });
return;
return NextResponse.json({ message: "grant type invalid" }, { status: 400 });
}
const [hashedSecret] = generateSecret(client_secret);
@@ -33,8 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
if (!client) {
res.status(401).json({ message: "Unauthorized" });
return;
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
const secretKey = process.env.CALENDSO_ENCRYPTION_KEY || "";
@@ -42,15 +34,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
let decodedRefreshToken: OAuthTokenPayload;
try {
const refreshToken = req.headers.get("authorization")?.split(" ")[1] || "";
decodedRefreshToken = jwt.verify(refreshToken, secretKey) as OAuthTokenPayload;
} catch {
res.status(401).json({ message: "Unauthorized" });
return;
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
if (!decodedRefreshToken || decodedRefreshToken.token_type !== "Refresh Token") {
res.status(401).json({ message: "Unauthorized" });
return;
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
const payload: OAuthTokenPayload = {
@@ -65,5 +56,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
expiresIn: 1800, // 30 min
});
res.status(200).json({ access_token });
return NextResponse.json({ access_token }, { status: 200 });
}
@@ -1,21 +1,16 @@
import jwt from "jsonwebtoken";
import type { NextApiRequest, NextApiResponse } from "next";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import prisma from "@calcom/prisma";
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
import type { OAuthTokenPayload } from "@calcom/types/oauth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
}
const { code, client_id, client_secret, grant_type, redirect_uri } = req.body;
export async function POST(req: NextRequest) {
const { code, client_id, client_secret, grant_type, redirect_uri } = await req.json();
if (grant_type !== "authorization_code") {
res.status(400).json({ message: "grant_type invalid" });
return;
return NextResponse.json({ message: "grant_type invalid" }, { status: 400 });
}
const [hashedSecret] = generateSecret(client_secret);
@@ -31,8 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
if (!client || client.redirectUri !== redirect_uri) {
res.status(401).json({ message: "Unauthorized" });
return;
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
const accessCode = await prisma.accessCode.findFirst({
@@ -45,7 +39,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
//delete all expired accessCodes + the one that is used here
// Delete all expired accessCodes + the one that is used here
await prisma.accessCode.deleteMany({
where: {
OR: [
@@ -63,8 +57,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
if (!accessCode) {
res.status(401).json({ message: "Unauthorized" });
return;
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
const secretKey = process.env.CALENDSO_ENCRYPTION_KEY || "";
@@ -93,5 +86,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
expiresIn: 30 * 24 * 60 * 60, // 30 days
});
res.status(200).json({ access_token, refresh_token });
return NextResponse.json({ access_token, refresh_token }, { status: 200 });
}
+41
View File
@@ -0,0 +1,41 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { HttpError } from "@calcom/lib/http-error";
// This is the callback endpoint for the OIDC provider
// A team must set this endpoint in the OIDC provider's configuration
async function handler(req: NextRequest) {
const { searchParams } = req.nextUrl;
const code = searchParams.get("code");
const state = searchParams.get("state");
if (!code || !state) {
return NextResponse.json({ message: "Code and state are required" }, { status: 400 });
}
const { oauthController } = await jackson();
try {
const { redirect_url } = await oauthController.oidcAuthzResponse({ code, state });
if (!redirect_url) {
throw new HttpError({
message: "No redirect URL found",
statusCode: 500,
});
}
return NextResponse.redirect(redirect_url, 302);
} catch (err) {
const { message, statusCode = 500 } = err as HttpError;
return NextResponse.json({ message }, { status: statusCode });
}
}
const getHandler = defaultResponderForAppDir(handler);
export { getHandler as GET };
@@ -1,4 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
@@ -13,11 +14,10 @@ const passwordResetRequestSchema = z.object({
requestId: z.string(), // format doesn't matter.
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Bad Method when not POST
if (req.method !== "POST") return res.status(405).end();
const { password: rawPassword, requestId: rawRequestId } = passwordResetRequestSchema.parse(req.body);
async function handler(req: NextRequest) {
const { password: rawPassword, requestId: rawRequestId } = passwordResetRequestSchema.parse(
await req.json()
);
// rate-limited there is a low, very low chance that a password request stays valid long enough
// to brute force 3.8126967e+40 options.
const maybeRequest = await prisma.resetPasswordRequest.findFirstOrThrow({
@@ -54,12 +54,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
} catch (e) {
return res.status(404).end();
return NextResponse.json({}, { status: 404 });
}
await expireResetPasswordRequest(rawRequestId);
return res.status(201).json({ message: "Password reset." });
return NextResponse.json({ message: "Password reset." }, { status: 201 });
}
async function expireResetPasswordRequest(rawRequestId: string) {
@@ -73,3 +73,5 @@ async function expireResetPasswordRequest(rawRequestId: string) {
},
});
}
export { handler as POST };
@@ -0,0 +1,29 @@
import type { OAuthReq } from "@boxyhq/saml-jackson";
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import type { HttpError } from "@calcom/lib/http-error";
async function handler(req: NextRequest) {
const { oauthController } = await jackson();
try {
const { redirect_url } = await oauthController.authorize(
Object.fromEntries(req.nextUrl.searchParams) as unknown as OAuthReq
);
return NextResponse.redirect(redirect_url as string, 302);
} catch (err) {
const { message, statusCode = 500 } = err as HttpError;
return NextResponse.json({ message }, { status: statusCode });
}
}
const getHandler = defaultResponderForAppDir(handler);
export { getHandler as GET };
export const dynamic = "force-dynamic";
@@ -0,0 +1,21 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import jackson from "@calcom/features/ee/sso/lib/jackson";
async function handler(req: NextRequest) {
const { oauthController } = await jackson();
const { redirect_url } = await oauthController.samlResponse(await req.json());
if (redirect_url) {
return NextResponse.redirect(redirect_url, 302);
}
return NextResponse.json({ message: "No redirect URL provided" }, { status: 400 });
}
const postHandler = defaultResponderForAppDir(handler);
export { postHandler as POST };
+15
View File
@@ -0,0 +1,15 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import jackson from "@calcom/features/ee/sso/lib/jackson";
async function handler(req: NextRequest) {
const { oauthController } = await jackson();
const tokenResponse = await oauthController.token(await req.json());
return NextResponse.json(tokenResponse);
}
const postHandler = defaultResponderForAppDir(handler);
export { postHandler as POST };
@@ -0,0 +1,38 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import z from "zod";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { HttpError } from "@calcom/lib/http-error";
const extractAuthToken = (req: NextRequest) => {
const authHeader = req.headers.get("authorization");
const parts = (authHeader || "").split(" ");
if (parts.length > 1) return parts[1];
// check for query param
let arr: string[] = [];
const { access_token } = requestQuery.parse(Object.fromEntries(req.nextUrl.searchParams));
arr = arr.concat(access_token);
if (arr[0].length > 0) return arr[0];
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
};
const requestQuery = z.object({
access_token: z.string(),
});
async function handler(req: NextRequest) {
const { oauthController } = await jackson();
const token = extractAuthToken(req);
const userInfo = await oauthController.userInfo(token);
return NextResponse.json(userInfo);
}
const getHandler = defaultResponderForAppDir(handler);
export { getHandler as GET };
export const dynamic = "force-dynamic";
@@ -1,12 +1,12 @@
import type { NextApiRequest } from "next";
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import z from "zod";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { emailRegex } from "@calcom/lib/emailSchema";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
@@ -24,13 +24,13 @@ const querySchema = z.object({
}),
});
async function handler(req: NextApiRequest) {
async function handler(req: NextRequest) {
const userCount = await prisma.user.count();
if (userCount !== 0) {
throw new HttpError({ statusCode: 400, message: "No setup needed." });
}
const parsedQuery = querySchema.safeParse(req.body);
const parsedQuery = querySchema.safeParse(await req.json());
if (!parsedQuery.success) {
throw new HttpError({ statusCode: 422, message: parsedQuery.error.message });
}
@@ -54,9 +54,9 @@ async function handler(req: NextApiRequest) {
},
});
return { message: "First admin user created successfully." };
return NextResponse.json({ message: "First admin user created successfully." });
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(handler) }),
});
const postHandler = defaultResponderForAppDir(handler);
export { postHandler as POST };
@@ -1,4 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
@@ -8,68 +10,68 @@ import { totpAuthenticatorCheck } from "@calcom/lib/totp";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/client";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function POST(req: NextRequest) {
const body = await req.json();
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
const session = await getServerSession({ req });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
if (!session.user?.id) {
console.error("Session is missing a user id.");
return res.status(500).json({ error: ErrorCode.InternalServerError });
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
}
const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } });
if (!user) {
console.error(`Session references user that no longer exists.`);
return res.status(401).json({ message: "Not authenticated" });
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
if (!user.password?.hash && user.identityProvider === IdentityProvider.CAL) {
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
return NextResponse.json({ error: ErrorCode.UserMissingPassword }, { status: 400 });
}
if (!user.twoFactorEnabled) {
return res.json({ message: "Two factor disabled" });
return NextResponse.json({ message: "Two factor disabled" });
}
if (user.password?.hash && user.identityProvider === IdentityProvider.CAL) {
const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash);
const isCorrectPassword = await verifyPassword(body.password, user.password.hash);
if (!isCorrectPassword) {
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
return NextResponse.json({ error: ErrorCode.IncorrectPassword }, { status: 400 });
}
}
// if user has 2fa and using backup code
if (user.twoFactorEnabled && req.body.backupCode) {
// If user has 2FA and using backup code
if (user.twoFactorEnabled && body.backupCode) {
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error("Missing encryption key; cannot proceed with backup code login.");
throw new Error(ErrorCode.InternalServerError);
}
if (!user.backupCodes) {
return res.status(400).json({ error: ErrorCode.MissingBackupCodes });
return NextResponse.json({ error: ErrorCode.MissingBackupCodes }, { status: 400 });
}
const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, process.env.CALENDSO_ENCRYPTION_KEY));
// check if user-supplied code matches one
const index = backupCodes.indexOf(req.body.backupCode.replaceAll("-", ""));
const index = backupCodes.indexOf(body.backupCode.replaceAll("-", ""));
if (index === -1) {
return res.status(400).json({ error: ErrorCode.IncorrectBackupCode });
return NextResponse.json({ error: ErrorCode.IncorrectBackupCode }, { status: 400 });
}
// we delete all stored backup codes at the end, no need to do this here
// if user has 2fa and NOT using backup code, try totp
} else if (user.twoFactorEnabled) {
if (!req.body.code) {
return res.status(400).json({ error: ErrorCode.SecondFactorRequired });
// throw new Error(ErrorCode.SecondFactorRequired);
if (!body.code) {
return NextResponse.json({ error: ErrorCode.SecondFactorRequired }, { status: 400 });
}
if (!user.twoFactorSecret) {
@@ -78,7 +80,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error(`"Missing encryption key; cannot proceed with two factor login."`);
console.error("Missing encryption key; cannot proceed with two factor login.");
throw new Error(ErrorCode.InternalServerError);
}
@@ -91,14 +93,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
// If user has 2fa enabled, check if body.code is correct
const isValidToken = totpAuthenticatorCheck(req.body.code, secret);
const isValidToken = totpAuthenticatorCheck(body.code, secret);
if (!isValidToken) {
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
// throw new Error(ErrorCode.IncorrectTwoFactorCode);
return NextResponse.json({ error: ErrorCode.IncorrectTwoFactorCode }, { status: 400 });
}
}
// If it is, disable users 2fa
// Disable 2FA
await prisma.user.update({
where: {
id: session.user.id,
@@ -110,5 +111,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
return res.json({ message: "Two factor disabled" });
return NextResponse.json({ message: "Two factor disabled" });
}
@@ -0,0 +1,68 @@
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
import prisma from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function POST(req: NextRequest) {
const body = await req.json();
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
if (!session) {
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
if (!session.user?.id) {
console.error("Session is missing a user id.");
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
}
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
if (!user) {
console.error(`Session references user that no longer exists.`);
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
if (user.twoFactorEnabled) {
return NextResponse.json({ error: ErrorCode.TwoFactorAlreadyEnabled }, { status: 400 });
}
if (!user.twoFactorSecret) {
return NextResponse.json({ error: ErrorCode.TwoFactorSetupRequired }, { status: 400 });
}
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error("Missing encryption key; cannot proceed with two factor setup.");
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
}
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
if (secret.length !== 32) {
console.error(
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
);
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
}
const isValidToken = totpAuthenticatorCheck(body.code, secret);
if (!isValidToken) {
return NextResponse.json({ error: ErrorCode.IncorrectTwoFactorCode }, { status: 400 });
}
await prisma.user.update({
where: {
id: session.user.id,
},
data: {
twoFactorEnabled: true,
},
});
return NextResponse.json({ message: "Two-factor enabled" });
}
@@ -1,5 +1,7 @@
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { authenticator } from "otplib";
import qrcode from "qrcode";
@@ -10,54 +12,55 @@ import { symmetricEncrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function POST(req: NextRequest) {
const body = await req.json();
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
const session = await getServerSession({ req });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
if (!session.user?.id) {
console.error("Session is missing a user id.");
return res.status(500).json({ error: ErrorCode.InternalServerError });
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
}
const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } });
if (!user) {
console.error(`Session references user that no longer exists.`);
return res.status(401).json({ message: "Not authenticated" });
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
if (user.identityProvider !== IdentityProvider.CAL && !user.password?.hash) {
return res.status(400).json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled });
return NextResponse.json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled }, { status: 400 });
}
if (!user.password?.hash) {
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
return NextResponse.json({ error: ErrorCode.UserMissingPassword }, { status: 400 });
}
if (user.twoFactorEnabled) {
return res.status(400).json({ error: ErrorCode.TwoFactorAlreadyEnabled });
return NextResponse.json({ error: ErrorCode.TwoFactorAlreadyEnabled }, { status: 400 });
}
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error("Missing encryption key; cannot proceed with two factor setup.");
return res.status(500).json({ error: ErrorCode.InternalServerError });
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
}
const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash);
const isCorrectPassword = await verifyPassword(body.password, user.password.hash);
if (!isCorrectPassword) {
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
return NextResponse.json({ error: ErrorCode.IncorrectPassword }, { status: 400 });
}
// This generates a secret 32 characters in length. Do not modify the number of
// bytes without updating the sanity checks in the enable and login endpoints.
const secret = authenticator.generateSecret(20);
// generate backup codes with 10 character length
// Generate backup codes with 10 character length
const backupCodes = Array.from(Array(10), () => crypto.randomBytes(5).toString("hex"));
await prisma.user.update({
@@ -75,5 +78,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const keyUri = authenticator.keyuri(name, "Cal", secret);
const dataUri = await qrcode.toDataURL(keyUri);
return res.json({ secret, keyUri, dataUri, backupCodes });
return NextResponse.json({ secret, keyUri, dataUri, backupCodes });
}
@@ -1,4 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import dayjs from "@calcom/dayjs";
import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
@@ -10,19 +11,16 @@ import { BookingStatus, ReminderType } from "@calcom/prisma/enums";
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
export async function POST(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
res.status(401).json({ message: "Not authenticated" });
return;
}
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
const reminderIntervalMinutes = [48 * 60, 24 * 60, 3 * 60];
let notificationsSent = 0;
for (const interval of reminderIntervalMinutes) {
const bookings = await prisma.booking.findMany({
where: {
@@ -144,5 +142,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
notificationsSent++;
}
}
res.status(200).json({ notificationsSent });
return NextResponse.json({ notificationsSent });
}
@@ -0,0 +1,23 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import prisma from "@calcom/prisma";
export async function POST(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (![process.env.CRON_API_KEY, `Bearer ${process.env.CRON_SECRET}`].includes(`${apiKey}`)) {
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
const deleted = await prisma.calendarCache.deleteMany({
where: {
// Delete all cache entries that expired before now
expiresAt: {
lte: new Date(Date.now()),
},
},
});
return NextResponse.json({ ok: true, count: deleted.count });
}
@@ -1,4 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
@@ -19,16 +20,11 @@ const travelScheduleSelect = {
},
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
if (process.env.CRON_API_KEY !== apiKey) {
res.status(401).json({ message: "Not authenticated" });
return;
}
export async function POST(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
if (process.env.CRON_API_KEY !== apiKey) {
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
let timeZonesChanged = 0;
@@ -169,5 +165,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
res.status(200).json({ timeZonesChanged });
return NextResponse.json({ timeZonesChanged });
}
@@ -1,4 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import { TeamBilling } from "@calcom/ee/billing/teams";
@@ -8,20 +9,17 @@ const querySchema = z.object({
page: z.coerce.number().min(0).optional().default(0),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
export async function POST(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
res.status(401).json({ message: "Not authenticated" });
return;
}
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
let { page: pageNumber } = querySchema.parse(req.query);
let { page: pageNumber } = querySchema.parse(Object.fromEntries(request.nextUrl.searchParams));
while (true) {
const teams = await prisma.team.findMany({
@@ -51,5 +49,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
pageNumber++;
}
res.json({ ok: true });
return NextResponse.json({ ok: true });
}
@@ -1,5 +1,6 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
@@ -12,22 +13,17 @@ const querySchema = z.object({
page: z.coerce.number().min(0).optional().default(0),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
export async function POST(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
res.status(401).json({ message: "Not authenticated" });
return;
}
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
let { page: pageNumber } = querySchema.parse(req.query);
let { page: pageNumber } = querySchema.parse(Object.fromEntries(request.nextUrl.searchParams));
const firstDateOfMonth = new Date();
firstDateOfMonth.setDate(1);
@@ -314,5 +310,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
pageNumber++;
}
res.json({ ok: true });
return NextResponse.json({ ok: true });
}
@@ -1,4 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import logger from "@calcom/lib/logger";
@@ -14,15 +15,11 @@ const log = logger.getSubLogger({
* syncAppMeta makes sure any app metadata that has been replicated into the database
* remains synchronized with any changes made to the app config files.
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
export async function POST(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
res.status(401).json({ message: "Not authenticated" });
return;
}
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
log.info(`🧐 Checking DB apps are in-sync with app metadata`);
@@ -63,5 +60,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
res.json({ ok: true });
return NextResponse.json({ ok: true });
}
@@ -0,0 +1,17 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { handleWebhookScheduledTriggers } from "@calcom/features/webhooks/lib/handleWebhookScheduledTriggers";
import prisma from "@calcom/prisma";
export async function POST(req: NextRequest) {
const apiKey = req.headers.get("authorization") || req.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
await handleWebhookScheduledTriggers(prisma);
return NextResponse.json({ ok: true });
}
@@ -0,0 +1,3 @@
import { handler } from "@calcom/features/ee/workflows/api/scheduleEmailReminders";
export { handler as POST };
@@ -0,0 +1,3 @@
import { handler } from "@calcom/features/ee/workflows/api/scheduleSMSReminders";
export { handler as POST };
@@ -0,0 +1,3 @@
import { handler } from "@calcom/features/ee/workflows/api/scheduleWhatsappReminders";
export { handler as POST };
+75
View File
@@ -0,0 +1,75 @@
import { NextResponse } from "next/server";
import { renderEmail } from "@calcom/emails";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getTranslation } from "@calcom/lib/server/i18n";
/**
* This API endpoint is used for development purposes to preview email templates
*/
export async function GET() {
// Only allow in development mode
if (IS_PRODUCTION) {
return new NextResponse("Only for development purposes", {
status: 403,
});
}
const t = await getTranslation("en", "common");
// Render the email template
const emailHtml = await renderEmail("MonthlyDigestEmail", {
language: t,
Created: 12,
Completed: 13,
Rescheduled: 14,
Cancelled: 16,
mostBookedEvents: [
{
eventTypeId: 3,
eventTypeName: "Test1",
count: 3,
},
{
eventTypeId: 4,
eventTypeName: "Test2",
count: 5,
},
],
membersWithMostBookings: [
{
userId: 4,
user: {
id: 4,
name: "User1 name",
email: "email.com",
avatar: "none",
username: "User1",
},
count: 4,
},
{
userId: 6,
user: {
id: 6,
name: "User2 name",
email: "email2.com",
avatar: "none",
username: "User2",
},
count: 8,
},
],
admin: { email: "admin.com", name: "admin" },
team: { name: "Team1", id: 4 },
});
// Create a response with the HTML content
const response = new NextResponse(emailHtml);
// Set appropriate headers
response.headers.set("Content-Type", "text/html");
response.headers.set("Cache-Control", "no-cache, no-store, private, must-revalidate");
return response;
}
+37
View File
@@ -0,0 +1,37 @@
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { FUTURE_ROUTES_OVERRIDE_COOKIE_NAME as COOKIE_NAME } from "@calcom/lib/constants";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function GET() {
const headersList = headers();
const cookiesList = cookies();
const legacyReq = buildLegacyRequest(headersList, cookiesList);
const session = await getServerSession({ req: legacyReq });
if (!session || !session.user || !session.user.email) {
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
let redirectUrl = "/";
// We take you back where you came from if possible
const referer = headersList.get("referer");
if (referer) redirectUrl = referer;
const response = NextResponse.redirect(redirectUrl);
// If has the cookie, Opt-out of V2
if (cookiesList.has(COOKIE_NAME) && cookiesList.get(COOKIE_NAME)?.value === "1") {
response.cookies.set(COOKIE_NAME, "0", { maxAge: 0, path: "/" });
} else {
/* Opt-in to V2 */
response.cookies.set(COOKIE_NAME, "1", { path: "/" });
}
return response;
}
@@ -1,25 +1,28 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import { dub } from "@calcom/features/auth/lib/dub";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const session = await getServerSession({ req });
export async function POST() {
const headersList = headers();
const cookiesList = cookies();
const legacyReq = buildLegacyRequest(headersList, cookiesList);
const session = await getServerSession({ req: legacyReq });
if (!session?.user?.username) {
return res.status(401).json({ message: "Unauthorized" });
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
try {
const { shortLink } = await dub.links.get({
externalId: `ext_${session.user.id.toString()}`,
});
return res.status(200).json({ shortLink });
return NextResponse.json({ shortLink });
} catch (error) {
console.log("Referral link not found, creating...");
}
@@ -46,5 +49,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
return res.status(200).json({ shortLink });
return NextResponse.json({ shortLink });
}
+12
View File
@@ -0,0 +1,12 @@
import { headers } from "next/headers";
import { NextResponse } from "next/server";
export async function GET() {
const headersList = headers();
const country = headersList.get("x-vercel-ip-country") || "Unknown";
const response = NextResponse.json({ country });
response.headers.set("Cache-Control", "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400");
return response;
}
+30
View File
@@ -0,0 +1,30 @@
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import crypto from "node:crypto";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function GET() {
const headersList = headers();
const cookiesList = cookies();
const legacyReq = buildLegacyRequest(headersList, cookiesList);
const session = await getServerSession({ req: legacyReq });
const secret = process.env.INTERCOM_SECRET;
if (!session) {
return NextResponse.json({ message: "user not authenticated" }, { status: 401 });
}
if (!secret) {
return NextResponse.json({ message: "Intercom Identity Verification secret not set" }, { status: 400 });
}
const hmac = crypto.createHmac("sha256", secret);
hmac.update(String(session.user.id));
const hash = hmac.digest("hex");
return NextResponse.json({ hash });
}
+108
View File
@@ -0,0 +1,108 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { createContext } from "@calcom/trpc/server/createContext";
import { bookingsRouter } from "@calcom/trpc/server/routers/viewer/bookings/_router";
import type { UserProfile } from "@calcom/types/UserProfile";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { TRPCError } from "@trpc/server";
enum DirectAction {
ACCEPT = "accept",
REJECT = "reject",
}
const querySchema = z.object({
action: z.nativeEnum(DirectAction),
token: z.string(),
reason: z.string().optional(),
});
const decryptedSchema = z.object({
bookingUid: z.string(),
userId: z.number().int(),
});
// Move the sessionGetter function outside the GET function
const createSessionGetter = (userId: number) => async () => {
return {
user: {
id: userId,
username: "" /* Not used in this context */,
role: UserPermissionRole.USER,
/* Not used in this context */
profile: {
id: null,
organizationId: null,
organization: null,
username: "",
upId: "",
} satisfies UserProfile,
},
upId: "",
hasValidLicense: true,
expires: "" /* Not used in this context */,
};
};
async function handler(request: NextRequest) {
const url = new URL(request.url);
const searchParams = request.nextUrl.searchParams;
const { action, token, reason } = querySchema.parse(Object.fromEntries(searchParams.entries()));
const decryptedData = JSON.parse(
symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || "")
);
const { bookingUid, userId } = decryptedSchema.parse(decryptedData);
const booking = await prisma.booking.findUniqueOrThrow({
where: { uid: bookingUid },
});
const user = await prisma.user.findUniqueOrThrow({
where: { id: userId },
});
// Use the factory function instead of declaring inside the block
const sessionGetter = createSessionGetter(userId);
try {
/** @see https://trpc.io/docs/server-side-calls */
// Create a legacy request object for compatibility
const legacyReq = buildLegacyRequest(headers(), cookies());
const res = {} as any; // Response is still mocked as it's not used in this context
const ctx = await createContext({ req: legacyReq, res }, sessionGetter);
const caller = bookingsRouter.createCaller({
...ctx,
req: legacyReq,
res,
user: { ...user, locale: user?.locale ?? "en" },
});
await caller.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,
confirmed: action === DirectAction.ACCEPT,
reason,
});
} catch (e) {
let message = "Error confirming booking";
if (e instanceof TRPCError) message = (e as TRPCError).message;
return NextResponse.redirect(`${url.origin}/booking/${bookingUid}?error=${encodeURIComponent(message)}`);
}
return NextResponse.redirect(`${url.origin}/booking/${bookingUid}`);
}
export const GET = defaultResponderForAppDir(handler);
@@ -1,4 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
@@ -16,6 +18,8 @@ import {
} from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const log = logger.getSubLogger({ prefix: ["[api/logo]"] });
function removePort(url: string) {
@@ -123,10 +127,17 @@ async function getTeamLogos(subdomain: string, isValidOrgDomain: boolean) {
where: {
slug: subdomain,
...(isValidOrgDomain && {
metadata: {
path: ["isOrganization"],
equals: true,
},
OR: [
{
metadata: {
path: ["isOrganization"],
equals: true,
},
},
{
isOrganization: true,
},
],
}),
},
select: {
@@ -151,15 +162,23 @@ async function getTeamLogos(subdomain: string, isValidOrgDomain: boolean) {
/**
* This API endpoint is used to serve the logo associated with a team if no logo is found we serve our default logo
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { query } = req;
const parsedQuery = logoApiSchema.parse(query);
const { isValidOrgDomain } = orgDomainConfig(req);
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const parsedQuery = logoApiSchema.parse(Object.fromEntries(searchParams.entries()));
// Create a legacy request object for compatibility
const legacyReq = buildLegacyRequest(headers(), cookies());
const { isValidOrgDomain } = orgDomainConfig(legacyReq);
const hostname = request.headers.get("host");
if (!hostname) {
return NextResponse.json({ error: "No hostname" }, { status: 400 });
}
const hostname = req?.headers["host"];
if (!hostname) throw new Error("No hostname");
const domains = extractSubdomainAndDomain(hostname);
if (!domains) throw new Error("No domains");
if (!domains) {
return NextResponse.json({ error: "No domains" }, { status: 400 });
}
const [subdomain] = domains;
const teamLogos = await getTeamLogos(subdomain, isValidOrgDomain);
@@ -186,11 +205,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
res.setHeader("Content-Type", response.headers.get("content-type") as string);
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
res.send(buffer);
// Create a new response with the image buffer
const imageResponse = new NextResponse(buffer);
// Set the appropriate headers
imageResponse.headers.set("Content-Type", response.headers.get("content-type") || "image/png");
imageResponse.headers.set("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
return imageResponse;
} catch (error) {
res.statusCode = 404;
res.json({ error: "Failed fetching logo" });
return NextResponse.json({ error: "Failed fetching logo" }, { status: 404 });
}
}
@@ -1,25 +1,36 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { performance } from "@calcom/lib/server/perfObserver";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
let isCold = true;
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
export async function GET() {
const prePrismaDate = performance.now();
const prisma = (await import("@calcom/prisma")).default;
const preSessionDate = performance.now();
const session = await getServerSession({ req });
if (!session) return res.status(409).json({ message: "Unauthorized" });
// Create a legacy request object for compatibility
const legacyReq = buildLegacyRequest(headers(), cookies());
const session = await getServerSession({ req: legacyReq });
if (!session) {
return NextResponse.json({ message: "Unauthorized" }, { status: 409 });
}
const preUserDate = performance.now();
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
if (!user) return res.status(404).json({ message: "No user found" });
if (!user) {
return NextResponse.json({ message: "No user found" }, { status: 404 });
}
const lastUpdate = performance.now();
res.setHeader("x-is-cold", isCold.toString());
isCold = false;
return res.status(200).json({
// Create response with headers
const response = NextResponse.json({
message: `Hello ${user.name}`,
prePrismaDate,
prismaDuration: `Prisma took ${preSessionDate - prePrismaDate}ms`,
@@ -30,4 +41,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
lastUpdate,
wasCold: isCold,
});
// Set custom header
response.headers.set("x-is-cold", isCold.toString());
isCold = false;
return response;
}
+9
View File
@@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ message: "Please don't" }, { status: 400 });
}
export async function POST() {
return NextResponse.json({ message: "Please don't" }, { status: 400 });
}
@@ -1,5 +1,7 @@
import { createHmac } from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getRoomNameFromRecordingId, getBatchProcessorJobAccessLink } from "@calcom/app-store/dailyvideo/lib";
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
@@ -8,7 +10,6 @@ import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import {
getAllTranscriptsAccessLinkFromMeetingId,
getDownloadLinkOfCalVideoByRecordingId,
@@ -32,11 +33,7 @@ import {
const log = logger.getSubLogger({ prefix: ["daily-video-webhook-handler"] });
const computeSignature = (
hmacSecret: string,
reqBody: NextApiRequest["body"],
webhookTimestampHeader: string | string[] | undefined
) => {
const computeSignature = (hmacSecret: string, reqBody: any, webhookTimestampHeader: string | null) => {
const signature = `${webhookTimestampHeader}.${JSON.stringify(reqBody)}`;
const base64DecodedSecret = Buffer.from(hmacSecret, "base64");
const hmac = createHmac("sha256", base64DecodedSecret);
@@ -51,53 +48,53 @@ const getDownloadLinkOfCalVideo = async (recordingId: string) => {
return downloadLink;
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
export async function POST(request: NextRequest) {
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
return res.status(405).json({ message: "No SendGrid API key or email" });
return NextResponse.json({ message: "No SendGrid API key or email" }, { status: 405 });
}
if (testRequestSchema.safeParse(req.body).success) {
return res.status(200).json({ message: "Test request successful" });
const body = await request.json();
if (testRequestSchema.safeParse(body).success) {
return NextResponse.json({ message: "Test request successful" });
}
const headersList = headers();
const testMode = process.env.NEXT_PUBLIC_IS_E2E || process.env.INTEGRATION_TEST_MODE;
if (!testMode) {
const hmacSecret = process.env.DAILY_WEBHOOK_SECRET;
if (!hmacSecret) {
return res.status(405).json({ message: "No Daily Webhook Secret" });
return NextResponse.json({ message: "No Daily Webhook Secret" }, { status: 405 });
}
const computed_signature = computeSignature(hmacSecret, req.body, req.headers["x-webhook-timestamp"]);
const webhookTimestamp = headersList.get("x-webhook-timestamp");
const computed_signature = computeSignature(hmacSecret, body, webhookTimestamp);
if (req.headers["x-webhook-signature"] !== computed_signature) {
return res.status(403).json({ message: "Signature does not match" });
if (headersList.get("x-webhook-signature") !== computed_signature) {
return NextResponse.json({ message: "Signature does not match" }, { status: 403 });
}
}
log.debug(
"Daily video webhook Request Body:",
safeStringify({
body: req.body,
body,
})
);
try {
if (req.body?.type === "recording.ready-to-download") {
const recordingReadyResponse = recordingReadySchema.safeParse(req.body);
if (body?.type === "recording.ready-to-download") {
const recordingReadyResponse = recordingReadySchema.safeParse(body);
if (!recordingReadyResponse.success) {
return res.status(400).send({
message: "Invalid Payload",
});
return NextResponse.json({ message: "Invalid Payload" }, { status: 400 });
}
const { room_name, recording_id, status } = recordingReadyResponse.data.payload;
if (status !== "finished") {
return res.status(400).send({
message: "Recording not finished",
});
return NextResponse.json({ message: "Recording not finished" }, { status: 400 });
}
const bookingReference = await getBookingReference(room_name);
@@ -138,19 +135,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// Submit Transcription Batch Processor Job
await submitBatchProcessorTranscriptionJob(recording_id);
} catch (err) {
log.error("Failed to Submit Transcription Batch Processor Job:", safeStringify(err));
log.error("Failed to Submit Transcription Batch Processor Job:", safeStringify(err));
}
// send emails to all attendees only when user has team plan
await sendDailyVideoRecordingEmails(evt, downloadLink);
return res.status(200).json({ message: "Success" });
} else if (req.body.type === "meeting.ended") {
const meetingEndedResponse = meetingEndedSchema.safeParse(req.body);
return NextResponse.json({ message: "Success" });
} else if (body.type === "meeting.ended") {
const meetingEndedResponse = meetingEndedSchema.safeParse(body);
if (!meetingEndedResponse.success) {
return res.status(400).send({
message: "Invalid Payload",
});
return NextResponse.json({ message: "Invalid Payload" }, { status: 400 });
}
const { room, meeting_id } = meetingEndedResponse.data.payload;
@@ -159,7 +154,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const booking = await getBooking(bookingReference.bookingId as number);
if (!booking.eventType?.canSendCalVideoTranscriptionEmails) {
return res.status(200).json({
return NextResponse.json({
message: `Transcription emails are disabled for this event type ${booking.eventTypeId}`,
});
}
@@ -167,21 +162,19 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const transcripts = await getAllTranscriptsAccessLinkFromMeetingId(meeting_id);
if (!transcripts || !transcripts.length)
return res
.status(200)
.json({ message: `No Transcripts found for room name ${room} and meeting id ${meeting_id}` });
return NextResponse.json({
message: `No Transcripts found for room name ${room} and meeting id ${meeting_id}`,
});
const evt = await getCalendarEvent(booking);
await sendDailyVideoTranscriptEmails(evt, transcripts);
return res.status(200).json({ message: "Success" });
} else if (req.body?.type === "batch-processor.job-finished") {
const batchProcessorJobFinishedResponse = batchProcessorJobFinishedSchema.safeParse(req.body);
return NextResponse.json({ message: "Success" });
} else if (body?.type === "batch-processor.job-finished") {
const batchProcessorJobFinishedResponse = batchProcessorJobFinishedSchema.safeParse(body);
if (!batchProcessorJobFinishedResponse.success) {
return res.status(400).send({
message: "Invalid Payload",
});
return NextResponse.json({ message: "Invalid Payload" }, { status: 400 });
}
const { id, input } = batchProcessorJobFinishedResponse.data.payload;
@@ -217,23 +210,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
},
});
return res.status(200).json({ message: "Success" });
return NextResponse.json({ message: "Success" });
} else {
log.error("Invalid type in /recorded-daily-video", req.body);
return res.status(200).json({ message: "Invalid type in /recorded-daily-video" });
log.error("Invalid type in /recorded-daily-video", body);
return NextResponse.json({ message: "Invalid type in /recorded-daily-video" });
}
} catch (err) {
log.error("Error in /recorded-daily-video", err);
if (err instanceof HttpError) {
return res.status(err.statusCode).json({ message: err.message });
return NextResponse.json({ message: err.message }, { status: err.statusCode });
} else {
return res.status(500).json({ message: "something went wrong" });
return NextResponse.json({ message: "something went wrong" }, { status: 500 });
}
}
}
export default defaultHandler({
POST: Promise.resolve({ default: handler }),
});
@@ -0,0 +1,141 @@
import type { DirectorySyncEvent, DirectorySyncRequest } from "@boxyhq/saml-jackson";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import handleGroupEvents from "@calcom/features/ee/dsync/lib/handleGroupEvents";
import handleUserEvents from "@calcom/features/ee/dsync/lib/handleUserEvents";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { DIRECTORY_IDS_TO_LOG } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import prisma from "@calcom/prisma";
const log = logger.getSubLogger({ prefix: ["[scim]"] });
// Extract the auth token from the request headers
const extractAuthToken = (req: NextRequest): string | null => {
const authHeader = req.headers.get("authorization") || null;
return authHeader ? authHeader.split(" ")[1] : null;
};
// Handle the SCIM events
const handleEvents = async (event: DirectorySyncEvent) => {
log.debug("handleEvents", safeStringify(event));
const dSyncData = await prisma.dSyncData.findFirst({
where: {
directoryId: event.directory_id,
},
select: {
id: true,
organizationId: true,
},
});
if (!dSyncData) {
throw new Error("Directory sync data not found");
}
const { organizationId } = dSyncData;
if (!organizationId) {
throw new Error(`Org ID not found for dsync ${dSyncData.id}`);
}
if (event.event.includes("group")) {
handleGroupEvents(event, organizationId);
}
if (event.event === "user.created" || event.event === "user.updated") {
await handleUserEvents(event, organizationId);
}
};
// This is the handler for the SCIM API requests
export async function GET(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "GET", params.directory);
}
export async function POST(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "POST", params.directory);
}
export async function PUT(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "PUT", params.directory);
}
export async function PATCH(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "PATCH", params.directory);
}
export async function DELETE(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "DELETE", params.directory);
}
async function handleScimRequest(request: NextRequest, method: string, directoryParams?: string[]) {
const { dsyncController } = await jackson();
if (!directoryParams || directoryParams.length === 0) {
return NextResponse.json({ error: "Missing directory parameters" }, { status: 400 });
}
const [directoryId, path, resourceId] = directoryParams;
const shouldLog = DIRECTORY_IDS_TO_LOG.includes(directoryId);
const searchParams = request.nextUrl.searchParams;
if (shouldLog) {
console.log(
"SCIM API request",
safeStringify({
method,
url: request.url,
params: directoryParams,
searchParams: Object.fromEntries(searchParams.entries()),
body: request.body
? await request
.clone()
.json()
.catch(() => ({}))
: undefined,
})
);
}
let body: object | undefined = undefined;
try {
body = await request.json().catch(() => undefined);
} catch (e) {
log.error(`Error parsing SCIM event for directoryId ${directoryId} with error: ${e}`);
}
// Handle the SCIM API requests
const scimRequest: DirectorySyncRequest = {
method,
directoryId,
resourceId,
apiSecret: extractAuthToken(request),
resourceType: path === "Users" ? "users" : "groups",
body,
query: {
count: searchParams.get("count") ? parseInt(searchParams.get("count") as string) : undefined,
startIndex: searchParams.get("startIndex")
? parseInt(searchParams.get("startIndex") as string)
: undefined,
filter: searchParams.get("filter") as string,
},
};
const { status, data } = await dsyncController.requests.handle(scimRequest, handleEvents);
if (shouldLog) {
console.log(
"Response to SCIM",
safeStringify({
status,
data,
})
);
}
return NextResponse.json(data, { status });
}
+1 -1
View File
@@ -39,7 +39,7 @@ const genericSchema = z.object({
});
async function handler(req: NextRequest) {
const { searchParams } = new URL(`${req.url}`);
const { searchParams } = req.nextUrl;
const imageType = searchParams.get("type");
const [calFontData, interFontData, interFontMediumData] = await Promise.all([
+96
View File
@@ -0,0 +1,96 @@
import { createHmac } from "crypto";
import { headers } from "next/headers";
import { cookies } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import getRawBody from "raw-body";
import z from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { default as webPrisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const helpscoutRequestBodySchema = z.object({
customer: z.object({
email: emailSchema,
}),
});
/**
* API for Helpscout to retrieve key information about a user from a ticket
* Note: HelpScout expects a JSON with a `html` prop to show its content as HTML
*/
export async function POST(request: NextRequest) {
const hsSignature = request.headers.get("x-helpscout-signature");
if (!hsSignature) return NextResponse.json({ message: "Missing signature" }, { status: 400 });
if (!process.env.CALENDSO_ENCRYPTION_KEY)
return NextResponse.json({ message: "Missing encryption key" }, { status: 500 });
const legacyRequest = buildLegacyRequest(headers(), cookies());
// Get the raw request body
const rawBody = await getRawBody(legacyRequest);
try {
const parsedBody = helpscoutRequestBodySchema.safeParse(JSON.parse(rawBody.toString()));
if (!parsedBody.success) return NextResponse.json({ message: "Invalid request body" }, { status: 400 });
// Verify the signature
const calculatedSig = createHmac("sha1", process.env.CALENDSO_ENCRYPTION_KEY)
.update(rawBody)
.digest("base64");
if (hsSignature !== calculatedSig)
return NextResponse.json({ message: "Invalid signature" }, { status: 400 });
const user = await webPrisma.user.findFirst({
where: {
email: parsedBody.data.customer.email,
},
select: {
username: true,
id: true,
createdDate: true,
},
});
if (!user) return NextResponse.json({ html: "User not found" });
const lastBooking = await webPrisma.attendee.findFirst({
where: {
email: parsedBody.data.customer.email,
},
select: {
booking: {
select: {
createdAt: true,
},
},
},
orderBy: {
booking: {
createdAt: "desc",
},
},
});
return NextResponse.json({
html: `
<ul>
<li><b>Username:</b>&nbsp;${user.username}</li>
<li><b>Last booking:</b>&nbsp;${
lastBooking && lastBooking.booking
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
: "No info"
}</li>
<li><b>Account created:</b>&nbsp;${new Date(user.createdDate).toLocaleDateString("en-US")}</li>
</ul>
`,
});
} catch (error) {
console.error("Error processing HelpScout request:", error);
return NextResponse.json({ message: "Internal server error" }, { status: 500 });
}
}
@@ -1,11 +1,11 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import type Stripe from "stripe";
import { z } from "zod";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { _MembershipModel as Membership, _TeamModel as Team } from "@calcom/prisma/zod";
@@ -24,49 +24,58 @@ type CheckoutSessionMetadata = z.infer<typeof checkoutSessionMetadataSchema>;
export const schemaTeamReadPublic = Team.omit({});
export const schemaMembershipPublic = Membership.merge(z.object({ team: Team }).partial());
async function handler(req: NextApiRequest, res: NextApiResponse) {
const checkoutSession = await getCheckoutSession(req);
validateCheckoutSession(checkoutSession);
const checkoutSessionSubscription = getCheckoutSessionSubscription(checkoutSession);
const checkoutSessionMetadata = getCheckoutSessionMetadata(checkoutSession);
async function handler(request: NextRequest) {
try {
const { session_id } = querySchema.parse(Object.fromEntries(request.nextUrl.searchParams));
const finalizedTeam = await prisma.team.update({
where: { id: checkoutSessionMetadata.pendingPaymentTeamId },
data: {
pendingPayment: false,
members: {
create: {
userId: checkoutSessionMetadata.ownerId as number,
role: MembershipRole.OWNER,
accepted: true,
const checkoutSession = await getCheckoutSession(session_id);
validateCheckoutSession(checkoutSession);
const checkoutSessionSubscription = getCheckoutSessionSubscription(checkoutSession);
const checkoutSessionMetadata = getCheckoutSessionMetadata(checkoutSession);
const finalizedTeam = await prisma.team.update({
where: { id: checkoutSessionMetadata.pendingPaymentTeamId },
data: {
pendingPayment: false,
members: {
create: {
userId: checkoutSessionMetadata.ownerId as number,
role: MembershipRole.OWNER,
accepted: true,
},
},
metadata: {
paymentId: checkoutSession.id,
subscriptionId: checkoutSessionSubscription.id || null,
subscriptionItemId: checkoutSessionSubscription.items.data[0].id || null,
},
},
metadata: {
paymentId: checkoutSession.id,
subscriptionId: checkoutSessionSubscription.id || null,
subscriptionItemId: checkoutSessionSubscription.items.data[0].id || null,
},
},
include: { members: true },
});
include: { members: true },
});
const response = JSON.stringify(
{
const response = {
message: `Team created successfully. We also made user with ID=${checkoutSessionMetadata.ownerId} the owner of this team.`,
team: schemaTeamReadPublic.parse(finalizedTeam),
owner: schemaMembershipPublic.parse(finalizedTeam.members[0]),
},
null,
2
);
};
return res.status(200).send(response);
return NextResponse.json(response);
} catch (error) {
console.error("Error creating team:", error);
if (error instanceof HttpError) {
return NextResponse.json({ message: error.message }, { status: error.statusCode });
}
return NextResponse.json(
{ message: error instanceof Error ? error.message : "An unexpected error occurred" },
{ status: 500 }
);
}
}
async function getCheckoutSession(req: NextApiRequest) {
const { session_id } = querySchema.parse(req.query);
const checkoutSession = await stripe.checkout.sessions.retrieve(session_id, {
async function getCheckoutSession(sessionId: string) {
const checkoutSession = await stripe.checkout.sessions.retrieve(sessionId, {
expand: ["subscription"],
});
if (!checkoutSession) throw new HttpError({ statusCode: 404, message: "Checkout session not found" });
@@ -102,11 +111,7 @@ function getCheckoutSessionMetadata(
});
}
const checkoutSessionMetadata = parseCheckoutSessionMetadata.data;
return checkoutSessionMetadata;
return parseCheckoutSessionMetadata.data;
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(handler) }),
});
export const GET = defaultResponderForAppDir(handler);
@@ -1,5 +1,6 @@
import { OAuth2Client } from "googleapis-common";
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -9,14 +10,19 @@ const scopes = [
"https://www.googleapis.com/auth/admin.directory.customer.readonly",
];
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") {
export async function GET(request: NextRequest) {
try {
// Get appKeys from google-calendar
const { client_id, client_secret } = await getAppKeysFromSlug("google-calendar");
if (!client_id || typeof client_id !== "string")
return res.status(400).json({ message: "Google client_id missing." });
return NextResponse.json({ message: "Google client_id missing." }, { status: 400 });
if (!client_secret || typeof client_secret !== "string")
return res.status(400).json({ message: "Google client_secret missing." });
return NextResponse.json({ message: "Google client_secret missing." }, { status: 400 });
// Get teamId from query params
const teamId = request.nextUrl.searchParams.get("teamId");
// use different callback to normal calendar connection
const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`;
@@ -25,11 +31,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const authUrl = oAuth2Client.generateAuthUrl({
access_type: "offline",
scope: scopes,
prompt: "consent",
state: JSON.stringify({ teamId: req.query.teamId }),
state: JSON.stringify({ teamId }),
});
res.status(200).json({ url: authUrl });
return NextResponse.json({ url: authUrl });
} catch (error) {
console.error("Error generating Google Workspace auth URL:", error);
return NextResponse.json(
{ message: error instanceof Error ? error.message : "An unexpected error occurred" },
{ status: 500 }
);
}
}
@@ -0,0 +1,86 @@
import { OAuth2Client } from "googleapis-common";
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import prisma from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const stateSchema = z.object({
teamId: z.string(),
});
export async function GET(request: NextRequest) {
try {
const headersList = headers();
const cookiesList = cookies();
const legacyReq = buildLegacyRequest(headersList, cookiesList);
const session = await getServerSession({ req: legacyReq });
if (!session?.user?.id) {
return NextResponse.json({ message: "You must be logged in to do this" }, { status: 401 });
}
const code = request.nextUrl.searchParams.get("code");
const state = request.nextUrl.searchParams.get("state");
if (!state) {
return NextResponse.json({ message: "No state provided" }, { status: 400 });
}
const parsedState = stateSchema.parse(JSON.parse(state));
const { teamId } = parsedState;
await throwIfNotHaveAdminAccessToTeam({
teamId: Number(teamId) ?? null,
userId: session.user.id,
});
if (!code || typeof code !== "string") {
return NextResponse.json({ message: "`code` must be a string" }, { status: 400 });
}
const { client_id, client_secret } = await getAppKeysFromSlug("google-calendar");
if (!client_id || typeof client_id !== "string")
return NextResponse.json({ message: "Google client_id missing." }, { status: 400 });
if (!client_secret || typeof client_secret !== "string")
return NextResponse.json({ message: "Google client_secret missing." }, { status: 400 });
const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`;
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uri);
const credentials = await oAuth2Client.getToken(code);
await prisma.credential.create({
data: {
type: "google_workspace_directory",
key: credentials.res?.data,
userId: session.user.id,
},
});
let redirectUrl = `${WEBAPP_URL}/settings`;
if (teamId) {
redirectUrl = `${WEBAPP_URL}/settings/teams/${teamId}/members?inviteModal=true&bulk=true`;
}
const safeRedirectUrl = getSafeRedirectUrl(redirectUrl) ?? `${WEBAPP_URL}/teams`;
return NextResponse.redirect(safeRedirectUrl);
} catch (error) {
console.error("Error in Google Workspace callback:", error);
// Redirect to teams page on error
return NextResponse.redirect(`${WEBAPP_URL}/teams`);
}
}
+33
View File
@@ -0,0 +1,33 @@
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const bodySchema = z.object({
username: z.string(),
orgSlug: z.string().optional(),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { username, orgSlug } = bodySchema.parse(body);
const legacyReq = buildLegacyRequest(headers(), cookies());
// Get current org domain from request headers
const { currentOrgDomain } = orgDomainConfig(legacyReq);
const result = await checkUsername(username, currentOrgDomain || orgSlug);
return NextResponse.json(result);
} catch (error) {
console.error(error);
return NextResponse.json({ error: "Failed to check username availability" }, { status: 400 });
}
}
@@ -0,0 +1,149 @@
import { headers, cookies } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import prisma from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { createContext } from "@calcom/trpc/server/createContext";
import { bookingsRouter } from "@calcom/trpc/server/routers/viewer/bookings/_router";
import { createCallerFactory } from "@calcom/trpc/server/trpc";
import type { UserProfile } from "@calcom/types/UserProfile";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { TRPCError } from "@trpc/server";
enum DirectAction {
ACCEPT = "accept",
REJECT = "reject",
}
const querySchema = z.object({
action: z.nativeEnum(DirectAction),
token: z.string(),
bookingUid: z.string(),
userId: z.string(),
});
export async function GET(request: NextRequest) {
const url = new URL(request.url);
const queryParams = Object.fromEntries(request.nextUrl.searchParams.entries());
try {
const { action, token, bookingUid, userId } = querySchema.parse(queryParams);
if (action === DirectAction.REJECT) {
// Rejections should use POST method
return NextResponse.redirect(
`${url.origin}/booking/${bookingUid}?error=${encodeURIComponent("Rejection requires POST method")}`
);
}
return await handleBookingAction(action, token, bookingUid, userId, undefined, request);
} catch (error) {
const bookingUid = queryParams.bookingUid || "";
return NextResponse.redirect(
`${url.origin}/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`
);
}
}
export async function POST(request: NextRequest) {
const url = new URL(request.url);
const queryParams = Object.fromEntries(request.nextUrl.searchParams.entries());
try {
const { action, token, bookingUid, userId } = querySchema.parse(queryParams);
const body = await request.json().catch(() => ({}));
const { reason } = z.object({ reason: z.string().optional() }).parse(body || {});
return await handleBookingAction(action, token, bookingUid, userId, reason, request);
} catch (error) {
const bookingUid = queryParams.bookingUid || "";
return NextResponse.redirect(
`${url.origin}/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`
);
}
}
async function handleBookingAction(
action: DirectAction,
token: string,
bookingUid: string,
userId: string,
reason?: string,
request?: NextRequest
) {
const booking = await prisma.booking.findUnique({
where: { oneTimePassword: token },
});
if (!booking) {
return NextResponse.redirect(
`/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`
);
}
const user = await prisma.user.findUniqueOrThrow({
where: { id: Number(userId) },
});
/** We shape the session as required by tRPC router */
async function sessionGetter() {
return {
user: {
id: Number(userId),
username: "" /* Not used in this context */,
role: UserPermissionRole.USER,
/* Not used in this context */
profile: {
id: null,
organizationId: null,
organization: null,
username: "",
upId: "",
} satisfies UserProfile,
},
upId: "",
hasValidLicense: true,
expires: "" /* Not used in this context */,
};
}
try {
/** @see https://trpc.io/docs/server-side-calls */
const createCaller = createCallerFactory(bookingsRouter);
// Use buildLegacyRequest to create a request object compatible with Pages Router
const legacyReq = request ? buildLegacyRequest(headers(), cookies()) : ({} as any);
const res = {} as any;
const ctx = await createContext({ req: legacyReq, res }, sessionGetter);
const caller = createCaller({
...ctx,
req: legacyReq,
res,
user: { ...user, locale: user?.locale ?? "en" },
});
await caller.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,
confirmed: action === DirectAction.ACCEPT,
/** Ignored reason input unless we're rejecting */
reason: action === DirectAction.REJECT ? reason : undefined,
});
} catch (e) {
let message = "Error confirming booking";
if (e instanceof TRPCError) message = (e as TRPCError).message;
return NextResponse.redirect(`/booking/${booking.uid}?error=${encodeURIComponent(message)}`);
}
await prisma.booking.update({
where: { id: booking.id },
data: { oneTimePassword: null },
});
return NextResponse.redirect(`/booking/${booking.uid}`);
}
+6
View File
@@ -0,0 +1,6 @@
import { NextResponse } from "next/server";
import * as pjson from "package.json";
export async function GET() {
return NextResponse.json({ version: pjson.version });
}
@@ -0,0 +1,103 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import z from "zod";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { CREDENTIAL_SYNC_SECRET, CREDENTIAL_SYNC_SECRET_HEADER_NAME } from "@calcom/lib/constants";
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
const appCredentialWebhookRequestBodySchema = z.object({
// UserId of the cal.com user
userId: z.number().int(),
appSlug: z.string(),
// Keys should be AES256 encrypted with the CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY
keys: z.string(),
});
export async function POST(request: NextRequest) {
if (!APP_CREDENTIAL_SHARING_ENABLED) {
return NextResponse.json({ message: "Credential sharing is not enabled" }, { status: 403 });
}
const secretHeader = request.headers.get(CREDENTIAL_SYNC_SECRET_HEADER_NAME);
if (secretHeader !== CREDENTIAL_SYNC_SECRET) {
return NextResponse.json({ message: "Invalid credential sync secret" }, { status: 403 });
}
try {
const body = await request.json();
const reqBodyParsed = appCredentialWebhookRequestBodySchema.safeParse(body);
if (!reqBodyParsed.success) {
return NextResponse.json({ error: reqBodyParsed.error.issues }, { status: 400 });
}
const reqBody = reqBodyParsed.data;
const user = await prisma.user.findUnique({ where: { id: reqBody.userId } });
if (!user) {
return NextResponse.json({ message: "User not found" }, { status: 404 });
}
const app = await prisma.app.findUnique({
where: { slug: reqBody.appSlug },
select: { dirName: true },
});
if (!app) {
return NextResponse.json({ message: "App not found" }, { status: 404 });
}
const appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata];
if (!appMetadata) {
return NextResponse.json(
{ message: "App not found. Ensure that you have the correct app slug" },
{ status: 404 }
);
}
const keys = JSON.parse(
symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
);
// INFO: Can't use prisma upsert as we don't know the id of the credential
const appCredential = await prisma.credential.findFirst({
where: {
userId: reqBody.userId,
appId: appMetadata.slug,
},
select: {
id: true,
},
});
if (appCredential) {
await prisma.credential.update({
where: {
id: appCredential.id,
},
data: {
key: keys,
},
});
return NextResponse.json({ message: `Credentials updated for userId: ${reqBody.userId}` });
} else {
await prisma.credential.create({
data: {
key: keys,
userId: reqBody.userId,
appId: appMetadata.slug,
type: appMetadata.type,
},
});
return NextResponse.json({ message: `Credentials created for userId: ${reqBody.userId}` });
}
} catch (error) {
console.error("Error processing app credential webhook:", error);
return NextResponse.json({ message: "Internal server error" }, { status: 500 });
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ export default function EnterprisePage() {
{
icon: <Icon name="credit-card" className="h-5 w-5 text-green-500" />,
title: t("unified_billing"),
description: t("unified_billing_description"),
description: t("advanced_managed_events_description"),
},
{
icon: <Icon name="lock" className="h-5 w-5 text-purple-500" />,
+35 -11
View File
@@ -120,6 +120,28 @@ function BookingListItem(booking: BookingItemProps) {
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
const [isNoShowDialogOpen, setIsNoShowDialogOpen] = useState<boolean>(false);
const cardCharged = booking?.payment[0]?.success;
const attendeeList = booking.attendees.map((attendee) => {
return {
name: attendee.name,
email: attendee.email,
id: attendee.id,
noShow: attendee.noShow || false,
phoneNumber: attendee.phoneNumber,
};
});
const noShowMutation = trpc.viewer.markNoShow.useMutation({
onSuccess: async (data) => {
showToast(data.message, "success");
// Invalidate and refetch the bookings query to update the UI
await utils.viewer.bookings.invalidate();
},
onError: (err) => {
showToast(err.message, "error");
},
});
const mutation = trpc.viewer.bookings.confirm.useMutation({
onSuccess: (data) => {
if (data?.status === BookingStatus.REJECTED) {
@@ -287,11 +309,22 @@ function BookingListItem(booking: BookingItemProps) {
if (isBookingInPast || isOngoing) {
editBookingActions.push({
id: "no_show",
label: t("mark_as_no_show"),
label:
attendeeList.length === 1 && attendeeList[0].noShow ? t("unmark_as_no_show") : t("mark_as_no_show"),
onClick: () => {
// If there's only one attendee, mark them as no-show directly without showing the dialog
if (attendeeList.length === 1) {
const attendee = attendeeList[0];
noShowMutation.mutate({
bookingUid: booking.uid,
attendees: [{ email: attendee.email, noShow: !attendee.noShow }],
});
return;
}
setIsNoShowDialogOpen(true);
},
icon: "eye-off" as const,
icon: attendeeList.length === 1 && attendeeList[0].noShow ? "eye" : ("eye-off" as const),
});
}
@@ -427,15 +460,6 @@ function BookingListItem(booking: BookingItemProps) {
];
const showPendingPayment = paymentAppData.enabled && booking.payment.length && !booking.paid;
const attendeeList = booking.attendees.map((attendee) => {
return {
name: attendee.name,
email: attendee.email,
id: attendee.id,
noShow: attendee.noShow || false,
phoneNumber: attendee.phoneNumber,
};
});
return (
<>
@@ -14,11 +14,12 @@ import {
LocationType,
OrganizerDefaultConferencingAppType,
} from "@calcom/app-store/locations";
import PhoneInput from "@calcom/features/components/phone-input";
import type { LocationOption } from "@calcom/features/form/components/LocationSelect";
import LocationSelect from "@calcom/features/form/components/LocationSelect";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, Icon, Input, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
import { Button, Icon, Input, Dialog, DialogContent, DialogFooter, Form } from "@calcom/ui";
import { QueryCell } from "../../lib/QueryCell";
@@ -1,115 +0,0 @@
import type { SyntheticEvent } from "react";
import { useState } from "react";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, showToast } from "@calcom/ui";
const ChangePasswordSection = () => {
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const { t, isLocaleReady } = useLocale();
// hold display until the locale is loaded
if (!isLocaleReady) {
return null;
}
const errorMessages: { [key: string]: string } = {
[ErrorCode.IncorrectPassword]: t("current_incorrect_password"),
[ErrorCode.NewPasswordMatchesOld]: t("new_password_matches_old_password"),
};
async function changePasswordHandler(e: SyntheticEvent) {
e.preventDefault();
if (isSubmitting) {
return;
}
setIsSubmitting(true);
setErrorMessage(null);
try {
const response = await fetch("/api/auth/changepw", {
method: "PATCH",
body: JSON.stringify({ oldPassword, newPassword }),
headers: {
"Content-Type": "application/json",
},
});
if (response.status === 200) {
setOldPassword("");
setNewPassword("");
showToast(t("password_has_been_changed"), "success");
return;
}
const body = await response.json();
setErrorMessage(errorMessages[body.error] || `${t("something_went_wrong")}${t("please_try_again")}`);
} catch (err) {
console.error(t("error_changing_password"), err);
setErrorMessage(`${t("something_went_wrong")}${t("please_try_again")}`);
} finally {
setIsSubmitting(false);
}
}
return (
<>
<form className="divide-subtle divide-y lg:col-span-9" onSubmit={changePasswordHandler}>
<div className="py-6 lg:pb-5">
<div className="my-3">
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("change_password")}</h2>
</div>
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0">
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-1/2">
<label htmlFor="current_password" className="text-default block text-sm font-medium">
{t("current_password")}
</label>
<div className="mt-1">
<input
type="password"
value={oldPassword}
onInput={(e) => setOldPassword(e.currentTarget.value)}
name="current_password"
id="current_password"
required
className="border-default block w-full rounded-sm text-sm"
placeholder={t("your_old_password")}
/>
</div>
</div>
<div className="w-full sm:w-1/2">
<label htmlFor="new_password" className="text-default block text-sm font-medium">
{t("new_password")}
</label>
<div className="mt-1">
<input
type="password"
name="new_password"
id="new_password"
value={newPassword}
required
onInput={(e) => setNewPassword(e.currentTarget.value)}
className="border-default block w-full rounded-sm text-sm"
placeholder={t("super_secure_new_password")}
/>
</div>
</div>
</div>
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
<div className="flex py-8 sm:justify-end">
<Button color="secondary" type="submit">
{t("save")}
</Button>
</div>
</div>
</form>
</>
);
};
export default ChangePasswordSection;
@@ -9,10 +9,9 @@ import {
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { expectWebhookToHaveBeenCalledWith } from "@calcom/web/test/utils/bookingScenario/expects";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { NextRequest } from "next/server";
import { createMocks } from "node-mocks-http";
import { describe, afterEach, test, vi, beforeEach, beforeAll } from "vitest";
import { describe, afterEach, test, vi, beforeEach, beforeAll, expect } from "vitest";
import { appStoreMetadata } from "@calcom/app-store/apps.metadata.generated";
import { getRoomNameFromRecordingId, getBatchProcessorJobAccessLink } from "@calcom/app-store/dailyvideo/lib";
@@ -20,10 +19,44 @@ import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/lib/videoClient"
import prisma from "@calcom/prisma";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
import handler from "@calcom/web/pages/api/recorded-daily-video";
import * as recordedDailyVideoRoute from "@calcom/web/app/api/recorded-daily-video/route";
// Mock the next/headers module before importing the handler
vi.mock("next/headers", () => ({
headers: () => new Headers({ "content-type": "application/json" }),
cookies: () => ({
get: () => null,
getAll: () => [],
has: () => false,
}),
}));
// Mock NextResponse to handle the response properly
vi.mock("next/server", async () => {
const actual = (await vi.importActual("next/server")) as any;
return {
...actual,
NextResponse: {
json: (data: any, init?: ResponseInit) => {
return new Response(JSON.stringify(data), {
...init,
headers: {
...init?.headers,
"content-type": "application/json",
},
});
},
// Add other methods you might need
redirect: actual.NextResponse.redirect,
next: actual.NextResponse.next,
rewrite: actual.NextResponse.rewrite,
},
};
});
// Now import the handler after mocking
const { POST: handler } = recordedDailyVideoRoute;
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
beforeAll(() => {
// Setup env vars
vi.stubEnv("SENDGRID_API_KEY", "FAKE_SENDGRID_API_KEY");
@@ -112,6 +145,24 @@ const TRANSCRIPTION_ACCESS_LINK = {
],
};
// We may need to make this more globally available. Will move if we need it elsewhere
function createNextRequest(mockReq: ReturnType<typeof createMocks>["req"]): NextRequest {
// Create a Request object that NextRequest can wrap
const request = new Request("https://example.com/api/recorded-daily-video", {
method: mockReq.method,
headers: new Headers(mockReq.headers as Record<string, string>),
body: mockReq.body ? JSON.stringify(mockReq.body) : undefined,
});
// Create a NextRequest from the Request
const nextRequest = new NextRequest(request, {
ip: mockReq.socket?.remoteAddress || "127.0.0.1",
geo: { city: "", country: "", region: "" },
});
return nextRequest;
}
describe("Handler: /api/recorded-daily-video", () => {
beforeEach(() => {
fetchMock.resetMocks();
@@ -212,13 +263,23 @@ describe("Handler: /api/recorded-daily-video", () => {
download_link: recordingDownloadLink,
});
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
const { req } = createMocks({
method: "POST",
body: BATCH_PROCESSOR_JOB_FINSISHED_PAYLOAD,
prisma,
});
await handler(req, res);
const nextReq = createNextRequest(req);
// Handle the response differently
try {
const response = await handler(nextReq);
// For App Router, we just need to check if the response exists
expect(response).toBeDefined();
} catch (error) {
console.error("Handler error:", error);
throw error;
}
await expectWebhookToHaveBeenCalledWith(subscriberUrl, {
triggerEvent: WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED,
-1
View File
@@ -175,7 +175,6 @@ export const config = {
// Next.js Doesn't support spread operator in config matcher, so, we must list all paths explicitly here.
// https://github.com/vercel/next.js/discussions/42458
matcher: [
"/",
"/403",
"/500",
"/icons",
@@ -128,8 +128,10 @@ function BookingsContent({ status }: BookingsProps) {
userIds,
attendeeName,
attendeeEmail,
afterStartDate: dateRange?.startDate ?? undefined,
beforeEndDate: dateRange?.endDate ?? undefined,
afterStartDate: dateRange?.startDate
? dayjs(dateRange?.startDate).startOf("day").toISOString()
: undefined,
beforeEndDate: dateRange?.endDate ? dayjs(dateRange?.endDate).endOf("day").toISOString() : undefined,
},
},
{
@@ -389,8 +391,9 @@ function BookingsContent({ status }: BookingsProps) {
variant="compact"
ToolbarLeft={
<>
<DataTableFilters.AddFilterButton table={table} />
<DataTableFilters.AddFilterButton table={table} hideWhenFilterApplied />
<DataTableFilters.ActiveFilters table={table} />
<DataTableFilters.AddFilterButton table={table} variant="sm" showWhenFilterApplied />
<DataTableFilters.ClearFiltersButton />
</>
}
@@ -321,12 +321,12 @@ export default function Success(props: PageProps) {
const attendee = bookingInfo.attendees[0]?.name || bookingInfo.attendees[0]?.email || "Nameless";
const host = bookingInfo.user.name || bookingInfo.user.email;
if (isHost) {
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
return t(`${titlePrefix}emailed_host_and_attendees${titleSuffix}`, {
user: attendee,
});
}
if (isAttendee) {
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
return t(`${titlePrefix}emailed_host_and_attendees${titleSuffix}`, {
user: host,
});
}
@@ -335,7 +335,7 @@ export default function Success(props: PageProps) {
attendee,
});
}
return t(`emailed_you_and_attendees${titleSuffix}`);
return t(`emailed_host_and_attendees${titleSuffix}`);
}
// This is a weird case where the same route can be opened in booking flow as a success page or as a booking detail page from the app
+2 -1
View File
@@ -54,8 +54,9 @@ function InsightsPageContent() {
className="flex flex-wrap items-center gap-2"
data-testid={`insights-filters-${isAll}-${teamId}-${userId}`}>
<OrgTeamsFilter />
<DataTableFilters.AddFilterButton table={table} />
<DataTableFilters.AddFilterButton table={table} hideWhenFilterApplied />
<DataTableFilters.ActiveFilters table={table} />
<DataTableFilters.AddFilterButton table={table} variant="sm" showWhenFilterApplied />
<DataTableFilters.ClearFiltersButton exclude={["createdAt"]} />
<div className="grow" />
<Download />
@@ -32,6 +32,7 @@ import {
DialogTrigger,
Editor,
Form,
Icon,
ImageUploader,
Label,
PasswordField,
@@ -654,6 +655,9 @@ const ProfileForm = ({
/>
</div>
{extraField}
<p className="text-subtle mt-1 flex items-center gap-1 text-sm">
<Icon name="info" /> {t("tip_username_plus")}
</p>
<div className="mt-6">
<TextField label={t("full_name")} {...formMethods.register("name")} />
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.0.12",
"version": "5.0.14",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
-70
View File
@@ -1,70 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession({ req });
if (!session || !session.user || !session.user.email) {
res.status(401).json({ message: "Not authenticated" });
return;
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
password: true,
identityProvider: true,
},
});
if (!user) {
res.status(404).json({ message: "User not found" });
return;
}
if (user.identityProvider !== IdentityProvider.CAL) {
return res.status(400).json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled });
}
const oldPassword = req.body.oldPassword;
const newPassword = req.body.newPassword;
const currentPassword = user.password?.hash;
if (!currentPassword) {
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
}
const passwordsMatch = await verifyPassword(oldPassword, currentPassword);
if (!passwordsMatch) {
return res.status(403).json({ error: ErrorCode.IncorrectPassword });
}
if (oldPassword === newPassword) {
return res.status(400).json({ error: ErrorCode.NewPasswordMatchesOld });
}
const hashedPassword = await hashPassword(newPassword);
await prisma.userPassword.upsert({
where: {
userId: user.id,
},
create: {
hash: hashedPassword,
userId: user.id,
},
update: {
hash: hashedPassword,
},
});
res.status(200).json({ message: "Password updated successfully" });
}
-14
View File
@@ -1,14 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import isAuthorized from "@calcom/features/auth/lib/oAuthAuthorization";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const requiredScopes = ["READ_PROFILE"];
const account = await isAuthorized(req, requiredScopes);
if (!account) {
return res.status(401).json({ message: "Unauthorized" });
}
return res.status(201).json({ username: account.name });
}
-36
View File
@@ -1,36 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { HttpError } from "@calcom/lib/http-error";
// This is the callback endpoint for the OIDC provider
// A team must set this endpoint in the OIDC provider's configuration
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "GET") {
return res.status(400).send("Method not allowed");
}
const { code, state } = req.query as {
code: string;
state: string;
};
const { oauthController } = await jackson();
try {
const { redirect_url } = await oauthController.oidcAuthzResponse({ code, state });
if (!redirect_url) {
throw new HttpError({
message: "No redirect URL found",
statusCode: 500,
});
}
return res.redirect(302, redirect_url);
} catch (err) {
const { message, statusCode = 500 } = err as HttpError;
return res.status(statusCode).send(message);
}
}
-23
View File
@@ -1,23 +0,0 @@
import type { OAuthReq } from "@boxyhq/saml-jackson";
import type { NextApiRequest, NextApiResponse } from "next";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import type { HttpError } from "@calcom/lib/http-error";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { oauthController } = await jackson();
if (req.method !== "GET") {
return res.status(400).send("Method not allowed");
}
try {
const { redirect_url } = await oauthController.authorize(req.query as unknown as OAuthReq);
return res.redirect(302, redirect_url as string);
} catch (err) {
const { message, statusCode = 500 } = err as HttpError;
return res.status(statusCode).send(message);
}
}
-19
View File
@@ -1,19 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { oauthController } = await jackson();
const { redirect_url } = await oauthController.samlResponse(req.body);
if (redirect_url) {
res.redirect(302, redirect_url);
}
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
});
-14
View File
@@ -1,14 +0,0 @@
import type { NextApiRequest } from "next";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
async function postHandler(req: NextApiRequest) {
const { oauthController } = await jackson();
return await oauthController.token(req.body);
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
});
-35
View File
@@ -1,35 +0,0 @@
import type { NextApiRequest } from "next";
import z from "zod";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
const extractAuthToken = (req: NextApiRequest) => {
const authHeader = req.headers["authorization"];
const parts = (authHeader || "").split(" ");
if (parts.length > 1) return parts[1];
// check for query param
let arr: string[] = [];
const { access_token } = requestQuery.parse(req.query);
arr = arr.concat(access_token);
if (arr[0].length > 0) return arr[0];
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
};
const requestQuery = z.object({
access_token: z.string(),
});
async function getHandler(req: NextApiRequest) {
const { oauthController } = await jackson();
const token = extractAuthToken(req);
return await oauthController.userInfo(token);
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
});
@@ -1,66 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const session = await getServerSession({ req });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
if (!session.user?.id) {
console.error("Session is missing a user id.");
return res.status(500).json({ error: ErrorCode.InternalServerError });
}
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
if (!user) {
console.error(`Session references user that no longer exists.`);
return res.status(401).json({ message: "Not authenticated" });
}
if (user.twoFactorEnabled) {
return res.status(400).json({ error: ErrorCode.TwoFactorAlreadyEnabled });
}
if (!user.twoFactorSecret) {
return res.status(400).json({ error: ErrorCode.TwoFactorSetupRequired });
}
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error("Missing encryption key; cannot proceed with two factor setup.");
return res.status(500).json({ error: ErrorCode.InternalServerError });
}
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
if (secret.length !== 32) {
console.error(
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
);
return res.status(500).json({ error: ErrorCode.InternalServerError });
}
const isValidToken = totpAuthenticatorCheck(req.body.code, secret);
if (!isValidToken) {
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
}
await prisma.user.update({
where: {
id: session.user.id,
},
data: {
twoFactorEnabled: true,
},
});
return res.json({ message: "Two-factor enabled" });
}
@@ -1,22 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
if (![process.env.CRON_API_KEY, `Bearer ${process.env.CRON_SECRET}`].includes(`${apiKey}`)) {
res.status(401).json({ message: "Not authenticated" });
return;
}
const deleted = await prisma.calendarCache.deleteMany({
where: {
// Delete all cache entries that expired before now
expiresAt: {
lte: new Date(Date.now()),
},
},
});
res.json({ ok: true, count: deleted.count });
}
@@ -1 +0,0 @@
export { default } from "@calcom/features/webhooks/lib/cron";
@@ -1 +0,0 @@
export { default } from "@calcom/features/ee/workflows/api/scheduleEmailReminders";
@@ -1 +0,0 @@
export { default } from "@calcom/features/ee/workflows/api/scheduleSMSReminders";
@@ -1 +0,0 @@
export { default } from "@calcom/features/ee/workflows/api/scheduleWhatsappReminders";

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