Compare commits

..
Author SHA1 Message Date
GitHub Actions 90f55670dc chore: release v5.4.11
Post release / release (push) Canceled after 0s
2025-07-01 11:06:49 +00:00
Anik Dhabal Babu 9c53f392ca fix: Optional Question ' Leaves Variable Tag in Event Name (#22175) 2025-07-01 10:50:41 +00:00
Morganandcubic-dev-ai[bot] cc19a81875 chore: deprecate platform libraries 0.0.2 (#22149)
* chore: deprecate platform libraries 0.0.2

* fixup! chore: deprecate platform libraries 0.0.2

* Update packages/lib/server/repository/schedule.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Update apps/api/v2/src/ee/schedules/schedules_2024_04_15/services/schedules.service.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* chore: bump platform libs

* Update apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* chore: bump platform libs

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-07-01 10:23:58 +00:00
Benny Joo 67032ddaf1 chore: add test for updateProfilePhotoGoogle (#22169)
* fix: process base64 avatar image

* better name

* more

* fix import

* add test

* util

* clean up

* address
2025-07-01 11:06:57 +01:00
2fe71c3695 refactor: destination calendars when rescheduling (#21873)
* refactor: destination calendars when rescheduling

* test: add unit test for collective evnet type

* chore: remove log

* chore: bump libraries

* fix: test

* chore: bump libraries

* fix: tests

* reset lock file

* Revert "reset lock file"

This reverts commit 4d48b678778b0daa19624338b302478e4655d200.

* Revert "Revert "reset lock file""

This reverts commit a7c3120c926a0d37a2b55a6ff8d2a070969ec0f4.

* regenerate lock file

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2025-07-01 12:07:48 +03:00
21422fe027 feat: enhance API v2 exception filters with user context for improved Axiom observability (#22157)
* feat: enhance API v2 exception filters with user context for improved Axiom observability

- Add extractUserContext utility to safely extract user/org/team context from request
- Enhance all 5 exception filters (HTTP, TRPC, Prisma, Zod, Calendar) with user context
- Include userId, userEmail, organizationId, teamId in Axiom logs when available
- Improve observability for debugging and monitoring authenticated requests

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* chore: update yarn.lock from lint-staged hooks

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: use request.organization instead of request.organizationId

- Address GitHub comment feedback from ThyMinimalDev
- Extract organization ID from organization object for consistency
- Follows same pattern as user and team extraction

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: revert to using request.organizationId as requested in GitHub comment

- Change back from request.organization to request.organizationId
- This aligns with how auth strategies actually populate the request object
- Addresses GitHub comment from ThyMinimalDev

Co-Authored-By: morgan@cal.com <morgan@cal.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: morgan@cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-07-01 10:05:21 +03:00
Devanshu Sharma 95575291ff feat: add Finnish locale to Day.js locales (#22174) 2025-07-01 07:01:32 +00:00
KartikandKartik Saini 3e61a0633d prevent info icon from repositioning when text wraps on small screens (#22132)
Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-07-01 09:32:41 +05:30
abhix4 85c9a790f2 fix: prevent location input from losing focus after each key stroke (#22167)
* fix: prevent location input from losing focus after each key stroke

* typo
2025-07-01 03:54:35 +00:00
Omar López e17d55457f refactor: cookie domain logic in defaultCookies (#22168) 2025-06-30 20:21:16 +00:00
Benny Joo d8d8bc4d00 fix: Process base64 avatar image (#22165)
* fix: process base64 avatar image

* better name

* more

* fix import
2025-06-30 21:08:46 +01:00
Udit TakkarandAlex van Andel 55cc1aea59 perf: optimize listWithTeamHandler query (#22069)
* perf: optimize listWithTeamHandler query

* chore: update query

* chore: add accepted true

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

* test: add integration test for the sql query

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-06-30 18:35:49 +00:00
GitHub Actions ae383164bc chore: release v5.4.10
Post release / release (push) Canceled after 0s
2025-06-30 13:22:55 +00:00
GitHub Actions 0d8863fe8e chore: release v5.4.10 2025-06-30 13:02:06 +00:00
GitHub Actions 51f6654635 chore: release v5.5.0 2025-06-30 12:39:41 +00:00
Eunjae LeeandDevin AI fa11e10bcb feat: remove 3-attendee limitation from CSV export (#22092)
Co-authored-by: eunjae@cal.com <hey@eunjae.dev>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-06-30 12:28:54 +00:00
github-actions[bot] a8f5bd6fe9 chore: version packages (#22152)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-30 11:16:15 +00:00
Lauris Skraucis a1c725bb35 refactor: atoms peer deps (#22153) 2025-06-30 10:22:45 +00:00
Anik Dhabal BabuandHariom Balhara e4eeb11a61 fix: fix request reschedule - not allowing reschedule of cancelled booking (#22115)
* fix: fix broken request reschdule feature

* Update getServerSideProps.ts

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-06-30 09:56:45 +00:00
Udit TakkarandHariom 3b0bb25251 fix: status code or router/embed rate limiting (#21968)
* fix: status code or router embed rate limiting

* fix: type err

* fix: handle api v2

* fix: status code

* chore: remove unused

* chore: update unit test

* chore: unit test

* refactor: make res required

* chore: remove unused

* chore: add a wrapper in getserversideprops

* chore: remlove log

* Add ratelLimiting unit test

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
2025-06-30 14:58:44 +05:30
Lauris Skraucis d27490e6f2 fix: atoms dist size (#22148)
* fix: reduce atoms dist size

* test changesets entry

* bump version
2025-06-30 09:26:08 +00:00
Anik Dhabal BabuandUdit Takkar b3c191b553 fix: truncate zoom meeting agenda (#22137)
* fix truncate zoom agenda

* type error

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-06-30 08:45:13 +00:00
Anik Dhabal Babuandcubic-dev-ai[bot] 0b977e615d fix: exclude opt out message from body (#22100)
* fix: exclude opt out

* Update packages/features/ee/workflows/api/scheduleSMSReminders.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-06-30 08:17:36 +00:00
github-actions[bot]andLauris Skraucis 979e1fd28b chore: version packages (#22143)
* chore: version packages

* Update package.json

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
2025-06-30 07:51:11 +00:00
Lauris Skraucis 3437c4331d fix: changesets atoms size (#22134)
* fix: changesets atoms npm package size

* test changesets entry
2025-06-30 07:22:09 +00:00
61 changed files with 4178 additions and 1051 deletions
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
View File
View File
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+1 -2
View File
@@ -38,8 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.231",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.236",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
@@ -18,6 +18,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
@@ -33,13 +34,19 @@ import {
HttpStatus,
Delete,
Query,
Headers,
InternalServerErrorException,
ParseIntPipe,
} from "@nestjs/common";
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
import {
EVENT_TYPE_READ,
EVENT_TYPE_WRITE,
SUCCESS_STATUS,
X_CAL_CLIENT_ID,
} from "@calcom/platform-constants";
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries/event-types";
import { PrismaClient } from "@calcom/prisma";
@Controller({
@@ -51,7 +58,8 @@ import { PrismaClient } from "@calcom/prisma";
export class EventTypesController_2024_04_15 {
constructor(
private readonly eventTypesService: EventTypesService_2024_04_15,
private readonly prismaReadService: PrismaReadService
private readonly prismaReadService: PrismaReadService,
private readonly organizationsRepository: OrganizationsRepository
) {}
@Post("/")
@@ -109,19 +117,30 @@ export class EventTypesController_2024_04_15 {
async getPublicEventType(
@Param("username") username: string,
@Param("eventSlug") eventSlug: string,
@Query() queryParams: GetPublicEventTypeQueryParams_2024_04_15
@Query() queryParams: GetPublicEventTypeQueryParams_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<GetEventTypePublicOutput> {
try {
let orgSlug = queryParams.org;
if (clientId && !orgSlug && username.includes(`-${clientId}`)) {
const org = await this.organizationsRepository.findTeamIdAndSlugFromClientId(clientId).catch(() => null);
if (org) {
orgSlug = org.slug;
}
}
const event = await getPublicEvent(
username.toLowerCase(),
eventSlug,
queryParams.isTeamEvent,
queryParams.org || null,
orgSlug ?? null,
this.prismaReadService.prisma as unknown as PrismaClient,
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
true
);
return {
data: event,
status: SUCCESS_STATUS,
@@ -2,6 +2,7 @@ import { EventTypesController_2024_04_15 } from "@/ee/event-types/event-types_20
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
@@ -9,7 +10,14 @@ import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule],
imports: [
PrismaModule,
MembershipsModule,
TokensModule,
UsersModule,
SelectedCalendarsModule,
OrganizationsModule,
],
providers: [EventTypesRepository_2024_04_15, EventTypesService_2024_04_15],
controllers: [EventTypesController_2024_04_15],
exports: [EventTypesService_2024_04_15, EventTypesRepository_2024_04_15],
@@ -1721,7 +1721,7 @@ describe("Event types Endpoints", () => {
description: "undefined booking fields",
length: 40,
hidden: false,
slug: "undefined-booking-fields",
slug: `undefined-booking-fields-${randomString()}`,
locations: [],
schedulingType: SchedulingType.ROUND_ROBIN,
};
@@ -1751,6 +1751,14 @@ describe("Event types Endpoints", () => {
disableOnPrefill: false,
hidden: false,
},
{
disableOnPrefill: false,
hidden: true,
isDefault: true,
required: false,
slug: "attendeePhoneNumber",
type: "phone",
},
{
isDefault: true,
type: "radioInput",
@@ -46,11 +46,10 @@ export class SchedulesController_2024_04_15 {
@Body() bodySchedule: CreateScheduleInput_2024_04_15
): Promise<CreateScheduleOutput_2024_04_15> {
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
return {
status: SUCCESS_STATUS,
data: scheduleFormatted,
data: schedule,
};
}
@@ -60,13 +59,10 @@ export class SchedulesController_2024_04_15 {
@GetUser() user: UserWithProfile
): Promise<GetDefaultScheduleOutput_2024_04_15 | null> {
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
const scheduleFormatted = schedule
? await this.schedulesService.formatScheduleForAtom(user, schedule)
: null;
return {
status: SUCCESS_STATUS,
data: scheduleFormatted,
data: schedule,
};
}
@@ -77,23 +73,25 @@ export class SchedulesController_2024_04_15 {
@Param("scheduleId") scheduleId: number
): Promise<GetScheduleOutput_2024_04_15> {
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
return {
status: SUCCESS_STATUS,
data: scheduleFormatted,
data: schedule,
};
}
@Get("/")
@Permissions([SCHEDULE_READ])
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_04_15> {
const schedules = await this.schedulesService.getUserSchedules(user.id);
const schedulesFormatted = await this.schedulesService.formatSchedulesForAtom(user, schedules);
const schedules = await this.schedulesService.getUserSchedules(
user.id,
user.timeZone,
user.defaultScheduleId
);
return {
status: SUCCESS_STATUS,
data: schedulesFormatted,
data: schedules,
};
}
@@ -1,26 +1,22 @@
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output";
import { SchedulesRepository_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.repository";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { Schedule } from "@prisma/client";
import { User } from "@prisma/client";
import type { ScheduleWithAvailabilities } from "@calcom/platform-libraries-0.0.2";
import { updateScheduleHandler } from "@calcom/platform-libraries-0.0.2";
import {
transformWorkingHoursForClient,
transformAvailabilityForClient,
transformDateOverridesForClient,
} from "@calcom/platform-libraries-0.0.2";
import { updateSchedule } from "@calcom/platform-libraries/schedules";
import { ScheduleRepository } from "@calcom/platform-libraries/schedules";
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
@Injectable()
export class SchedulesService_2024_04_15 {
constructor(
private readonly schedulesRepository: SchedulesRepository_2024_04_15,
private readonly usersRepository: UsersRepository
private readonly usersRepository: UsersRepository,
private readonly dbWrite: PrismaWriteService
) {}
async createUserDefaultSchedule(userId: number, timeZone: string) {
@@ -48,7 +44,9 @@ export class SchedulesService_2024_04_15 {
await this.usersRepository.setDefaultSchedule(userId, createdSchedule.id);
}
return createdSchedule;
const formattedSchedule = await this.getUserSchedule(userId, createdSchedule.id);
return formattedSchedule;
}
async getUserScheduleDefault(userId: number) {
@@ -56,11 +54,29 @@ export class SchedulesService_2024_04_15 {
if (!user?.defaultScheduleId) return null;
return this.schedulesRepository.getScheduleById(user.defaultScheduleId);
return await ScheduleRepository.findDetailedScheduleById({
scheduleId: user.defaultScheduleId,
isManagedEventType: undefined,
userId,
timeZone: user.timeZone,
defaultScheduleId: user.defaultScheduleId,
});
}
async getUserSchedule(userId: number, scheduleId: number) {
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
const user = await this.usersRepository.findById(userId);
if (!user) {
throw new NotFoundException(`User with ID=${userId} does not exist.`);
}
const existingSchedule = await ScheduleRepository.findDetailedScheduleById({
scheduleId: scheduleId,
isManagedEventType: undefined,
userId,
timeZone: user.timeZone,
defaultScheduleId: user.defaultScheduleId,
});
if (!existingSchedule) {
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
@@ -71,8 +87,8 @@ export class SchedulesService_2024_04_15 {
return existingSchedule;
}
async getUserSchedules(userId: number) {
return this.schedulesRepository.getSchedulesByUserId(userId);
async getUserSchedules(userId: number, timeZone: string, defaultScheduleId: number | null) {
return ScheduleRepository.findManyDetailedScheduleByUserId({ userId, timeZone, defaultScheduleId });
}
async updateUserSchedule(
@@ -89,7 +105,6 @@ export class SchedulesService_2024_04_15 {
this.checkUserOwnsSchedule(user.id, existingSchedule);
const schedule = await this.getUserSchedule(user.id, Number(scheduleId));
const scheduleFormatted = await this.formatScheduleForAtom(user, schedule);
if (!bodySchedule.schedule) {
// note(Lauris): When updating an availability in cal web app, lets say only its name, also
@@ -97,12 +112,16 @@ export class SchedulesService_2024_04_15 {
// and they have same shape, so to match shapes I attach "scheduleFormatted.availability" to reflect
// schedule that would be passed by the web app. If we don't, then updating schedule name will erase
// schedule.
bodySchedule.schedule = scheduleFormatted.availability;
bodySchedule.schedule = schedule.availability;
}
return updateScheduleHandler({
input: { scheduleId: Number(scheduleId), ...bodySchedule },
ctx: { user },
return updateSchedule({
input: {
scheduleId: Number(scheduleId),
...bodySchedule,
},
user,
prisma: this.dbWrite.prisma as unknown as PrismaClient,
});
}
@@ -118,44 +137,6 @@ export class SchedulesService_2024_04_15 {
return this.schedulesRepository.deleteScheduleById(scheduleId);
}
async formatScheduleForAtom(user: User, schedule: ScheduleWithAvailabilities): Promise<ScheduleOutput> {
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
return this.transformScheduleForAtom(schedule, usersSchedulesCount, user);
}
async formatSchedulesForAtom(
user: User,
schedules: ScheduleWithAvailabilities[]
): Promise<ScheduleOutput[]> {
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
return Promise.all(
schedules.map((schedule) => this.transformScheduleForAtom(schedule, usersSchedulesCount, user))
);
}
async transformScheduleForAtom(
schedule: ScheduleWithAvailabilities,
userSchedulesCount: number,
user: Pick<User, "id" | "defaultScheduleId" | "timeZone">
): Promise<ScheduleOutput> {
const timeZone = schedule.timeZone || user.timeZone;
const defaultSchedule = await this.getUserScheduleDefault(user.id);
return {
id: schedule.id,
name: schedule.name,
isManaged: schedule.userId !== user.id,
workingHours: transformWorkingHoursForClient(schedule),
schedule: schedule.availability,
availability: transformAvailabilityForClient(schedule),
timeZone,
dateOverrides: transformDateOverridesForClient(schedule, timeZone),
isDefault: defaultSchedule?.id === schedule.id,
isLastSchedule: userSchedulesCount <= 1,
readOnly: schedule.userId !== user.id,
};
}
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">) {
if (userId !== schedule.userId) {
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
@@ -1,3 +1,4 @@
import { extractUserContext } from "@/lib/extract-user-context";
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
import { Request } from "express";
@@ -39,6 +40,7 @@ export class CalendarServiceExceptionFilter implements ExceptionFilter {
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
response.setHeader("X-Request-Id", requestId.toString());
const userContext = extractUserContext(request);
this.logger.error(`Calendar Service Exception Filter: ${exception?.message}`, {
exception,
body: request.body,
@@ -46,6 +48,7 @@ export class CalendarServiceExceptionFilter implements ExceptionFilter {
url: request.url,
method: request.method,
requestId,
...userContext,
});
response.status(400).json({
@@ -1,3 +1,4 @@
import { extractUserContext } from "@/lib/extract-user-context";
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
import { Request } from "express";
@@ -16,6 +17,7 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
const statusCode = exception.getStatus();
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
response.setHeader("X-Request-Id", requestId.toString());
const userContext = extractUserContext(request);
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
exception,
body: request.body,
@@ -23,6 +25,7 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
url: request.url,
method: request.method,
requestId,
...userContext,
});
response.status(statusCode).json({
@@ -1,3 +1,4 @@
import { extractUserContext } from "@/lib/extract-user-context";
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
import { Catch, HttpStatus, Logger } from "@nestjs/common";
@@ -43,6 +44,7 @@ export class PrismaExceptionFilter implements ExceptionFilter {
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
response.setHeader("X-Request-Id", requestId.toString());
const userContext = extractUserContext(request);
this.logger.error(`PrismaError: ${error.message}`, {
error,
body: request.body,
@@ -50,6 +52,7 @@ export class PrismaExceptionFilter implements ExceptionFilter {
url: request.url,
method: request.method,
requestId,
...userContext,
});
let message = "There was an error, please try again later.";
@@ -1,3 +1,4 @@
import { extractUserContext } from "@/lib/extract-user-context";
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
import { Request } from "express";
@@ -120,6 +121,7 @@ export class TRPCExceptionFilter implements ExceptionFilter {
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
response.setHeader("X-Request-Id", requestId.toString());
const userContext = extractUserContext(request);
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
exception,
body: request.body,
@@ -127,6 +129,7 @@ export class TRPCExceptionFilter implements ExceptionFilter {
url: request.url,
method: request.method,
requestId,
...userContext,
});
response.status(statusCode).json({
@@ -1,3 +1,4 @@
import { extractUserContext } from "@/lib/extract-user-context";
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
import { Catch, HttpStatus, Logger } from "@nestjs/common";
@@ -18,6 +19,7 @@ export class ZodExceptionFilter implements ExceptionFilter {
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
response.setHeader("X-Request-Id", requestId.toString());
const userContext = extractUserContext(request);
this.logger.error(`ZodError: ${error.message}`, {
error,
body: request.body,
@@ -25,6 +27,7 @@ export class ZodExceptionFilter implements ExceptionFilter {
url: request.url,
method: request.method,
requestId,
...userContext,
});
response.status(HttpStatus.BAD_REQUEST).json({
@@ -0,0 +1,34 @@
import { Request } from "express";
import { Team } from "@calcom/prisma/client";
import { ApiAuthGuardUser } from "../modules/auth/strategies/api-auth/api-auth.strategy";
export interface UserContext {
userId?: number;
userEmail?: string;
organizationId?: number;
teamId?: number;
}
export function extractUserContext(request: Request): UserContext {
const context: UserContext = {};
const user = (request as any).user as ApiAuthGuardUser | undefined;
if (user) {
context.userId = user.id;
context.userEmail = user.email;
}
const organizationId = (request as any).organizationId as number | undefined;
if (organizationId) {
context.organizationId = organizationId;
}
const team = (request as any).team as Team | undefined;
if (team) {
context.teamId = team.id;
}
return context;
}
@@ -69,7 +69,7 @@ export class OrganizationsRepository {
return id;
}
async findTeamIdFromClientId(clientId: string) {
async findTeamIdAndSlugFromClientId(clientId: string) {
return this.dbRead.prisma.team.findFirstOrThrow({
where: {
platformOAuthClient: {
@@ -80,6 +80,7 @@ export class OrganizationsRepository {
},
select: {
id: true,
slug: true,
},
});
}
@@ -117,8 +117,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
// A booking that has been rescheduled to a new booking will also have a status of CANCELLED
const isDisabledRescheduling = booking.eventType?.disableRescheduling;
// This comes from query param and thus is considered forced
const canRescheduleCancelledBooking =
isForcedRescheduleForCancelledBooking || booking.eventType?.allowReschedulingCancelledBookings;
const canBookThroughCancelledBookingRescheduleLink = booking.eventType?.allowReschedulingCancelledBookings;
const isNonRescheduleableBooking =
booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED;
@@ -131,8 +130,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
};
}
if (isNonRescheduleableBooking) {
const canReschedule = booking.status === BookingStatus.CANCELLED && canRescheduleCancelledBooking;
if (isNonRescheduleableBooking && !isForcedRescheduleForCancelledBooking) {
const canReschedule =
booking.status === BookingStatus.CANCELLED && canBookThroughCancelledBookingRescheduleLink;
return {
redirect: {
destination: canReschedule ? eventUrl : `/booking/${uid}`,
@@ -652,8 +652,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 className="text-subtle mt-1 flex gap-1 text-sm">
<Icon name="info" className="mt-0.5 flex-shrink-0" />
<span className="flex-1">{t("tip_username_plus")}</span>
</p>
<div className="mt-6">
<TextField label={t("full_name")} {...formMethods.register("name")} />
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.4.9",
"version": "5.4.11",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+1 -1
View File
@@ -12,7 +12,7 @@ export default function Router({ form, message, errorMessage }: inferSSRProps<ty
return (
<>
<Head>
<title>{form.name} | Cal.com Forms</title>
<title>{form?.name} | Cal.com Forms</title>
</Head>
<div className="mx-auto my-0 max-w-3xl md:my-24">
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
@@ -1,11 +1,30 @@
import { wrapGetServerSidePropsWithSentry } from "@sentry/nextjs";
import type { GetServerSidePropsContext } from "next";
import { getRoutedUrl } from "@calcom/lib/server/getRoutedUrl";
import { getRoutedUrl, hasEmbedPath } from "@calcom/lib/server/getRoutedUrl";
import { TRPCError } from "@trpc/server";
export const getServerSideProps = wrapGetServerSidePropsWithSentry(async function getServerSideProps(
context: GetServerSidePropsContext
) {
return await getRoutedUrl(context);
try {
return await getRoutedUrl(context);
} catch (error) {
if (error instanceof TRPCError && error.code === "TOO_MANY_REQUESTS") {
context.res.statusCode = 429;
const isEmbed = hasEmbedPath(context.req.url || "");
return {
props: {
isEmbed,
form: null,
message: null,
errorMessage: error.message,
},
};
}
throw error;
}
},
"/router");
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,114 @@
import { oauth2_v2 } from "@googleapis/oauth2";
import type { OAuth2Client } from "googleapis-common";
import { describe, it, expect, vi, beforeEach } from "vitest";
import logger from "@calcom/lib/logger";
import { uploadAvatar } from "@calcom/lib/server/avatar";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
import { updateProfilePhotoGoogle } from "./updateProfilePhotoGoogle";
vi.mock("@googleapis/oauth2", () => ({
oauth2_v2: {
Oauth2: vi.fn(),
},
}));
vi.mock("@calcom/lib/logger", () => ({
default: {
error: vi.fn(),
},
}));
vi.mock("@calcom/lib/server/avatar", () => ({
uploadAvatar: vi.fn(),
}));
vi.mock("@calcom/lib/server/resizeBase64Image", () => ({
resizeBase64Image: vi.fn(),
}));
vi.mock("@calcom/lib/server/repository/user", () => ({
UserRepository: {
updateAvatar: vi.fn(),
},
}));
describe("updateProfilePhotoGoogle", () => {
const mockOAuth2Client = {} as OAuth2Client;
const userId = 123;
let mockOauth2Instance: any;
beforeEach(() => {
vi.clearAllMocks();
mockOauth2Instance = {
userinfo: {
get: vi.fn(),
},
};
(oauth2_v2.Oauth2 as any).mockImplementation(() => mockOauth2Instance);
(uploadAvatar as any).mockResolvedValue("/api/avatar/processed-123.png");
(resizeBase64Image as any).mockResolvedValue("data:image/png;base64,resized-data");
(UserRepository.updateAvatar as any).mockResolvedValue(undefined);
});
it("should update avatar with valid URL", async () => {
const validUrl = "https://example.com/avatar.jpg";
mockOauth2Instance.userinfo.get.mockResolvedValue({
data: { picture: validUrl },
});
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
expect(UserRepository.updateAvatar).toHaveBeenCalledWith({
id: userId,
avatarUrl: validUrl,
});
expect(uploadAvatar).not.toHaveBeenCalled();
});
it("should process base64 data", async () => {
const base64Data =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
mockOauth2Instance.userinfo.get.mockResolvedValue({
data: { picture: base64Data },
});
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
expect(resizeBase64Image).toHaveBeenCalledWith(base64Data);
expect(uploadAvatar).toHaveBeenCalledWith({
avatar: "data:image/png;base64,resized-data",
userId,
});
expect(UserRepository.updateAvatar).toHaveBeenCalledWith({
id: userId,
avatarUrl: "/api/avatar/processed-123.png",
});
});
it("should do nothing when no picture provided", async () => {
mockOauth2Instance.userinfo.get.mockResolvedValue({
data: {},
});
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
expect(UserRepository.updateAvatar).not.toHaveBeenCalled();
expect(uploadAvatar).not.toHaveBeenCalled();
});
it("should handle errors gracefully", async () => {
const error = new Error("OAuth API failed");
mockOauth2Instance.userinfo.get.mockRejectedValue(error);
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
expect(logger.error).toHaveBeenCalledWith("Error updating avatarUrl from google calendar connect", error);
expect(UserRepository.updateAvatar).not.toHaveBeenCalled();
});
});
@@ -1,16 +1,32 @@
import { oauth2_v2 } from "@googleapis/oauth2";
import type { OAuth2Client } from "googleapis-common";
import { isBase64Image } from "@calcom/lib/isBase64Image";
import logger from "@calcom/lib/logger";
import { uploadAvatar } from "@calcom/lib/server/avatar";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
export async function updateProfilePhotoGoogle(oAuth2Client: OAuth2Client, userId: number) {
try {
const oauth2 = new oauth2_v2.Oauth2({ auth: oAuth2Client });
const userDetails = await oauth2.userinfo.get();
if (userDetails.data?.picture) {
await UserRepository.updateAvatar({ id: userId, avatarUrl: userDetails.data.picture });
const avatarUrl = userDetails.data?.picture;
if (!avatarUrl) {
return;
}
// Handle base64 data
if (isBase64Image(avatarUrl)) {
const resizedAvatarUrl = await uploadAvatar({
avatar: await resizeBase64Image(avatarUrl),
userId,
});
await UserRepository.updateAvatar({ id: userId, avatarUrl: resizedAvatarUrl });
return;
}
await UserRepository.updateAvatar({ id: userId, avatarUrl });
} catch (error) {
logger.error("Error updating avatarUrl from google calendar connect", error);
}
@@ -4,7 +4,12 @@ import type { NextApiRequest, NextApiResponse } from "next";
import GoogleCalendarService from "@calcom/app-store/googlecalendar/lib/CalendarService";
import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar";
import { GOOGLE_CALENDAR_SCOPES, WEBAPP_URL, WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
import {
GOOGLE_CALENDAR_SCOPES,
SCOPE_USERINFO_PROFILE,
WEBAPP_URL,
WEBAPP_URL_FOR_OAUTH,
} from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
@@ -14,6 +19,7 @@ import { Prisma } from "@calcom/prisma/client";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
import { updateProfilePhotoGoogle } from "../../_utils/oauth/updateProfilePhotoGoogle";
import { getGoogleAppKeys } from "../lib/getGoogleAppKeys";
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
@@ -95,10 +101,9 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
}
// Only attempt to update the user's profile photo if the user has granted the required scope
// TODO: Use the avatarUrl when setting the profile picture
// if (grantedScopes.includes(SCOPE_USERINFO_PROFILE)) {
// await updateProfilePhotoGoogle(oAuth2Client, req.session.user.id);
// }
if (grantedScopes.includes(SCOPE_USERINFO_PROFILE)) {
await updateProfilePhotoGoogle(oAuth2Client, req.session.user.id);
}
const selectedCalendarWhereUnique = {
userId: req.session.user.id,
@@ -155,6 +155,18 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
};
};
// Zoom agenda field has a 2000 character limit; we set maxLength to 1900 to leave a safety buffer
const truncateAgenda = (description?: string | null) => {
if (!description) return description;
const maxLength = 1900;
const trimmed = description.trimEnd();
if (trimmed.length > maxLength) {
return `${trimmed.substring(0, maxLength).trimEnd()}...`;
}
return trimmed;
};
const userSettings = await getUserSettings();
const recurrence = getRecurrence(event);
// Documentation at: https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
@@ -166,7 +178,7 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
//schedule_for: "string", TODO: Used when scheduling the meeting for someone else (needed?)
timezone: event.organizer.timeZone,
password: userSettings?.schedule_meeting?.default_password_for_scheduled_meetings ?? undefined,
agenda: event.description,
agenda: truncateAgenda(event.description),
settings: {
host_video: true,
participant_video: true,
+1
View File
@@ -4,6 +4,7 @@ import "dayjs/locale/cs";
import "dayjs/locale/de";
import "dayjs/locale/es";
import "dayjs/locale/es-mx";
import "dayjs/locale/fi";
import "dayjs/locale/fr";
import "dayjs/locale/he";
import "dayjs/locale/hu";
@@ -1502,15 +1502,6 @@ async function handler(
evt.videoCallData = undefined;
// To prevent "The requested identifier already exists" error while updating event, we need to remove iCalUID
evt.iCalUID = undefined;
} else {
// In case of rescheduling, we need to keep the previous host destination calendar
evt = CalendarEventBuilder.fromEvent(evt)
.withDestinationCalendar(
originalRescheduledBooking?.destinationCalendar
? [originalRescheduledBooking?.destinationCalendar]
: evt.destinationCalendar
)
.build();
}
const updateManager = await eventManager.reschedule(
@@ -0,0 +1,240 @@
import {
createBookingScenario,
getGoogleCalendarCredential,
TestData,
getOrganizer,
getBooker,
getScenarioData,
mockSuccessfulVideoMeetingCreation,
mockCalendarToHaveNoBusySlots,
getDate,
BookingLocations,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { expectBookingToBeInDatabase } from "@calcom/web/test/utils/bookingScenario/expects";
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { describe, expect, beforeEach } from "vitest";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { resetTestEmails } from "@calcom/lib/testEmails";
import { SchedulingType } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "@calcom/web/test/fixtures/fixtures";
export type CustomNextApiRequest = NextApiRequest & Request;
export type CustomNextApiResponse = NextApiResponse & Response;
const timeout = process.env.CI ? 5000 : 20000;
describe("handleNewBooking", () => {
setupAndTeardown();
beforeEach(() => {
resetTestEmails();
});
describe("Collective Event Reschedule with Destination Calendar Bug", () => {
test(
`should reschedule a collective event booking and ensure both hosts have the booking in their calendars when a second host is added
1. Create a collective event with 1 host and book it
2. Add a second host to the event type
3. Reschedule the booking
4. Verify both hosts have the booking in their calendars
`,
async ({ emails }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: "google_calendar",
externalId: "organizer@google-calendar.com",
primaryEmail: "organizer@google-calendar.com",
},
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP";
const iCalUID = `${uidOfBookingToBeRescheduled}@Cal.com`;
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 15,
length: 30,
schedulingType: SchedulingType.COLLECTIVE,
users: [
{
id: 101, // organizer
},
{
id: 102, // second host (will be added to event type)
},
],
destinationCalendar: {
integration: "google_calendar",
externalId: "event-type-1@google-calendar.com",
primaryEmail: "event-type-1@google-calendar.com",
},
},
],
users: [
{
id: 101,
name: "Organizer",
email: "organizer@example.com",
username: "organizer",
timeZone: "Asia/Kolkata",
defaultScheduleId: null,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: "google_calendar",
externalId: "organizer@google-calendar.com",
primaryEmail: "organizer@google-calendar.com",
},
},
{
id: 102,
name: "Second Host",
email: "second-host@example.com",
username: "second-host",
timeZone: "Asia/Kolkata",
defaultScheduleId: null,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: "google_calendar",
externalId: "second-host@google-calendar.com",
primaryEmail: "second-host@google-calendar.com",
},
},
],
bookings: [
{
uid: uidOfBookingToBeRescheduled,
eventTypeId: 1,
status: BookingStatus.ACCEPTED,
startTime: `${plus1DateString}T05:00:00.000Z`,
endTime: `${plus1DateString}T05:30:00.000Z`,
users: [
{
id: 101, // organizer
},
],
references: [
{
type: appStoreMetadata.dailyvideo.type,
uid: "MOCK_ID_1",
meetingId: "MOCK_ID_1",
meetingPassword: "MOCK_PASS_1",
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
credentialId: null,
},
{
type: appStoreMetadata.googlecalendar.type,
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_1",
meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_1",
meetingPassword: "MOCK_PASSWORD",
meetingUrl: "https://UNUSED_URL",
externalCalendarId: "event-type-1@google-calendar.com",
credentialId: undefined,
},
],
iCalUID,
},
],
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: appStoreMetadata.dailyvideo.dirName,
videoMeetingData: {
id: "MOCK_ID_1", // Video meeting ID is not updated during reschedule
password: "MOCK_PASS_1",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
const organizerCalendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
update: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_2",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID_2",
},
});
const rescheduleBookingData = getMockRequestDataForBooking({
data: {
start: `${plus2DateString}T11:00:00.000Z`,
end: `${plus2DateString}T11:30:00.000Z`,
eventTypeId: 1,
rescheduleUid: uidOfBookingToBeRescheduled,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
},
rescheduledBy: organizer.email,
},
});
const rescheduledBooking = await handleNewBooking({
bookingData: rescheduleBookingData,
});
await expectBookingToBeInDatabase({
description: "",
location: BookingLocations.CalVideo,
responses: expect.objectContaining({
email: booker.email,
name: booker.name,
}),
uid: rescheduledBooking.uid!,
eventTypeId: rescheduleBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
references: [
{
type: appStoreMetadata.dailyvideo.type,
uid: "MOCK_ID_1",
meetingId: "MOCK_ID_1",
meetingPassword: "MOCK_PASS_1",
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
type: appStoreMetadata.googlecalendar.type,
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_1",
externalCalendarId: "event-type-1@google-calendar.com",
},
],
});
expect(organizerCalendarMock.updateEventCalls.length).toBe(1);
expect(organizerCalendarMock.createEventCalls.length).toBe(0);
// Check availability calls for both hosts
expect(organizerCalendarMock.getAvailabilityCalls.length).toBe(2);
},
timeout
);
});
});
@@ -157,6 +157,8 @@ export async function handler(req: NextRequest) {
}
if (message?.length && message?.length > 0 && sendTo) {
const smsMessageWithoutOptOut = message;
if (process.env.TWILIO_OPT_OUT_ENABLED === "true") {
message = await WorkflowOptOutService.addOptOutMessage(message, locale || "en");
}
@@ -167,6 +169,7 @@ export async function handler(req: NextRequest) {
body: message,
scheduledDate: reminder.scheduledDate,
sender: senderID,
bodyWithoutOptOut: smsMessageWithoutOptOut,
bookingUid: reminder.booking.uid,
userId,
teamId,
@@ -14,6 +14,7 @@ export async function sendSmsOrFallbackEmail(props: {
phoneNumber: string;
body: string;
sender: string;
bodyWithoutOptOut?: string;
bookingUid?: string | null;
userId?: number | null;
teamId?: number | null;
@@ -40,7 +41,7 @@ export async function sendSmsOrFallbackEmail(props: {
await sendOrScheduleWorkflowEmails({
to: [fallbackData.email],
subject: fallbackData.t("notification_about_your_booking"),
html: twilioData.body,
html: twilioData.bodyWithoutOptOut || twilioData.body,
replyTo: fallbackData.replyTo,
});
}
@@ -60,6 +61,7 @@ export async function scheduleSmsOrFallbackEmail(props: {
body: string;
scheduledDate: Date;
sender: string;
bodyWithoutOptOut?: string;
bookingUid?: string | null;
userId?: number | null;
teamId?: number | null;
@@ -96,7 +98,7 @@ export async function scheduleSmsOrFallbackEmail(props: {
await sendOrScheduleWorkflowEmails({
to: [fallbackData.email],
subject: fallbackData.t("notification_about_your_booking"),
html: twilioData.body,
html: twilioData.bodyWithoutOptOut || twilioData.body,
replyTo: fallbackData.replyTo,
sendAt: twilioData.scheduledDate,
referenceUid: reminder.uuid || undefined,
@@ -176,6 +176,8 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => {
}
if (smsMessage.length > 0) {
const smsMessageWithoutOptOut = smsMessage;
if (process.env.TWILIO_OPT_OUT_ENABLED === "true") {
smsMessage = await WorkflowOptOutService.addOptOutMessage(
smsMessage,
@@ -196,6 +198,7 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => {
phoneNumber: reminderPhone,
body: smsMessage,
sender: senderID,
bodyWithoutOptOut: smsMessageWithoutOptOut,
bookingUid: evt.uid,
userId,
teamId,
@@ -107,6 +107,66 @@ const getLocationInfo = ({
return { locationAvailable, locationDetails };
};
const LocationInput = (props: {
eventLocationType: EventLocationType;
defaultValue?: string;
index: number;
customClassNames?: LocationInputCustomClassNames;
disableLocationProp?: boolean;
}) => {
const { t } = useLocale();
const { eventLocationType, index, customClassNames, disableLocationProp, ...remainingProps } = props;
if (eventLocationType?.organizerInputType === "text") {
const { defaultValue, ...rest } = remainingProps;
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
<Input
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
type="text"
required
onChange={onChange}
value={value}
{...(disableLocationProp ? { disabled: true } : {})}
className={classNames("my-0", customClassNames?.addressInput)}
{...rest}
/>
);
}}
/>
);
} else if (eventLocationType?.organizerInputType === "phone") {
const { defaultValue, ...rest } = remainingProps;
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
<PhoneInput
required
disabled={disableLocationProp}
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
className={customClassNames?.phoneInput}
value={value}
onChange={onChange}
{...rest}
/>
);
}}
/>
);
}
return null;
};
const Locations: React.FC<LocationsProps> = ({
isChildrenManagedEventType,
disableLocationProp,
@@ -168,64 +228,6 @@ const Locations: React.FC<LocationsProps> = ({
locationOptions: props.locationOptions,
});
const LocationInput = (props: {
eventLocationType: EventLocationType;
defaultValue?: string;
index: number;
customClassNames?: LocationInputCustomClassNames;
}) => {
const { eventLocationType, index, customClassNames, ...remainingProps } = props;
if (eventLocationType?.organizerInputType === "text") {
const { defaultValue, ...rest } = remainingProps;
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
<Input
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
type="text"
required
onChange={onChange}
value={value}
{...(disableLocationProp ? { disabled: true } : {})}
className={classNames("my-0", customClassNames?.addressInput)}
{...rest}
/>
);
}}
/>
);
} else if (eventLocationType?.organizerInputType === "phone") {
const { defaultValue, ...rest } = remainingProps;
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
<PhoneInput
required
disabled={disableLocationProp}
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
className={customClassNames?.phoneInput}
value={value}
onChange={onChange}
{...rest}
/>
);
}}
/>
);
}
return null;
};
const [showEmptyLocationSelect, setShowEmptyLocationSelect] = useState(false);
const defaultInitialLocation = defaultValue || null;
const [selectedNewOption, setSelectedNewOption] = useState<SingleValueLocationOption | null>(
@@ -459,6 +461,7 @@ const Locations: React.FC<LocationsProps> = ({
eventLocationType={eventLocationType}
index={index}
customClassNames={customClassNames?.organizerContactInput?.locationInput}
disableLocationProp={disableLocationProp}
/>
</div>
<ErrorMessage
+38 -16
View File
@@ -484,16 +484,34 @@ class EventsInsights {
: booking.attendees;
const formattedAttendees = attendeeList
.slice(0, 3)
.map((attendee) => (attendee ? `${attendee.name} (${attendee.email})` : null));
.map((attendee) => (attendee ? `${attendee.name} (${attendee.email})` : null))
.filter(Boolean);
return [
booking.uid,
{ attendeeList: formattedAttendees, noShowGuest: attendeeList[0]?.noShow || false },
];
})
);
const maxAttendees = Math.max(
...Array.from(bookingMap.values()).map((data) => data.attendeeList.length),
0
);
const finalBookingMap = new Map(
Array.from(bookingMap.entries()).map(([uid, data]) => {
const attendeeFields: Record<string, string | null> = {};
for (let i = 1; i <= maxAttendees; i++) {
attendeeFields[`attendee${i}`] = data.attendeeList[i - 1] || null;
}
return [
uid,
{
noShowGuest: attendeeList[0]?.noShow || false,
attendee1: formattedAttendees[0] || null,
attendee2: formattedAttendees[1] || null,
attendee3: formattedAttendees[2] || null,
noShowGuest: data.noShowGuest,
...attendeeFields,
},
];
})
@@ -502,33 +520,37 @@ class EventsInsights {
const data = csvData.map((bookingTimeStatus) => {
if (!bookingTimeStatus.uid) {
// should not be reached because we filtered above
const nullAttendeeFields: Record<string, null> = {};
for (let i = 1; i <= maxAttendees; i++) {
nullAttendeeFields[`attendee${i}`] = null;
}
return {
...bookingTimeStatus,
noShowGuest: false,
attendee1: null,
attendee2: null,
attendee3: null,
...nullAttendeeFields,
};
}
const attendeeData = bookingMap.get(bookingTimeStatus.uid);
const attendeeData = finalBookingMap.get(bookingTimeStatus.uid);
if (!attendeeData) {
const nullAttendeeFields: Record<string, null> = {};
for (let i = 1; i <= maxAttendees; i++) {
nullAttendeeFields[`attendee${i}`] = null;
}
return {
...bookingTimeStatus,
noShowGuest: false,
attendee1: null,
attendee2: null,
attendee3: null,
...nullAttendeeFields,
};
}
return {
...bookingTimeStatus,
noShowGuest: attendeeData.noShowGuest,
attendee1: attendeeData.attendee1,
attendee2: attendeeData.attendee2,
attendee3: attendeeData.attendee3,
...Object.fromEntries(Object.entries(attendeeData).filter(([key]) => key.startsWith("attendee"))),
};
});
+1 -8
View File
@@ -1,7 +1,5 @@
import type { CookieOption, CookiesOptions } from "next-auth";
import { isENVDev } from "@calcom/lib/env";
/**
* Copy from 'https://github.com/nextauthjs/next-auth/blob/227ff2259f/src/core/lib/cookie.ts' as we can't import it directly
*
@@ -20,12 +18,7 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
const cookiePrefix = useSecureCookies ? "__Secure-" : "";
const defaultOptions: CookieOption["options"] = {
domain: isENVDev
? process.env.ORGANIZATIONS_ENABLED
? //FIXME: This is causing login to not work if someone uses anything other .cal.local for testing
".cal.local"
: undefined
: NEXTAUTH_COOKIE_DOMAIN,
domain: NEXTAUTH_COOKIE_DOMAIN || undefined,
// To enable cookies on widgets,
// https://stackoverflow.com/questions/45094712/iframe-not-reading-cookies-in-chrome
// But we need to set it as `lax` in development
+2
View File
@@ -104,6 +104,8 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView
}
dynamicEventName = dynamicEventName.replace(`{${variable}}`, fieldValue || "");
} else {
dynamicEventName = dynamicEventName.replace(`{${variable}}`, "");
}
});
+12
View File
@@ -0,0 +1,12 @@
/**
* Checks if a string is a supported base64 image format
*/
export function isBase64Image(input: string | null | undefined): boolean {
if (!input) return false;
return (
input.startsWith("data:image/png;base64,") ||
input.startsWith("data:image/jpeg;base64,") ||
input.startsWith("data:image/jpg;base64,")
);
}
+17
View File
@@ -1,5 +1,6 @@
import "@calcom/lib/__mocks__/logger";
import { createHash } from "crypto";
import type { GetServerSidePropsContext } from "next";
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -12,12 +13,14 @@ import { substituteVariables } from "@calcom/app-store/routing-forms/lib/substit
import { getUrlSearchParamsToForward } from "@calcom/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { isAuthorizedToViewFormOnOrgDomain } from "@calcom/features/routing-forms/lib/isAuthorizedToViewForm";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { RoutingFormRepository } from "@calcom/lib/server/repository/routingForm";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { getRoutedUrl } from "./getRoutedUrl";
// Mock dependencies
vi.mock("@calcom/lib/checkRateLimitAndThrowError");
vi.mock("@calcom/app-store/routing-forms/lib/handleResponse");
vi.mock("@calcom/lib/server/repository/routingForm");
vi.mock("@calcom/lib/server/repository/user");
@@ -112,6 +115,7 @@ describe("getRoutedUrl", () => {
it("should return notFound if form is not found", async () => {
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(null);
const context = mockContext({});
const result = await getRoutedUrl(context);
expect(result).toEqual({ notFound: true });
expect(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).toHaveBeenCalledWith("form-id");
@@ -259,6 +263,19 @@ describe("getRoutedUrl", () => {
);
});
it("should throw an error if rate limit is exceeded", async () => {
vi.mocked(checkRateLimitAndThrowError).mockRejectedValue(new Error("Rate limit exceeded"));
const context = mockContext({ email: "test@cal.com" });
const expectedHash = createHash("sha256")
.update(JSON.stringify({ email: "test@cal.com" }))
.digest("hex");
await expect(getRoutedUrl(context)).rejects.toThrow("Rate limit exceeded");
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
identifier: `form:form-id:hash:${expectedHash}`,
});
});
describe("Dry Run", () => {
it("should call handleResponse with isPreview:true", async () => {
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(mockForm as never);
+1 -1
View File
@@ -44,7 +44,7 @@ const getDeterministicHashForResponse = (fieldsResponses: Record<string, unknown
return hash;
};
function hasEmbedPath(pathWithQuery: string) {
export function hasEmbedPath(pathWithQuery: string) {
const onlyPath = pathWithQuery.split("?")[0];
return onlyPath.endsWith("/embed") || onlyPath.endsWith("/embed/");
}
@@ -138,6 +138,70 @@ export class ScheduleRepository {
isDefault: !scheduleId || defaultScheduleId === schedule.id,
isLastSchedule: schedulesCount <= 1,
readOnly: schedule.userId !== userId && !isManagedEventType,
userId: schedule.userId,
};
}
static async findManyDetailedScheduleByUserId({
isManagedEventType,
userId,
defaultScheduleId,
timeZone: userTimeZone,
}: {
timeZone: string;
userId: number;
defaultScheduleId: number | null;
isManagedEventType?: boolean;
}) {
const schedules = await prisma.schedule.findMany({
where: {
userId: userId,
},
select: {
id: true,
userId: true,
name: true,
availability: true,
timeZone: true,
},
});
if (!schedules?.length) {
throw new Error("Schedules not found");
}
const isCurrentUserPartOfTeam = await hasReadPermissionsForUserId({
memberId: schedules[0].userId,
userId,
});
const schedulesFormatted = schedules.map((schedule) => {
const isCurrentUserOwner = schedule?.userId === userId;
if (!isCurrentUserPartOfTeam && !isCurrentUserOwner) {
throw new Error("UNAUTHORIZED");
}
const timeZone = schedule.timeZone || userTimeZone;
// disabling utc casting while fetching WorkingHours
return {
id: schedule.id,
name: schedule.name,
isManaged: schedule.userId !== userId,
workingHours: transformWorkingHoursForAtom(schedule),
schedule: schedule.availability,
availability: transformAvailabilityForAtom(schedule),
timeZone,
isDefault: schedule.id === defaultScheduleId,
dateOverrides: transformDateOverridesForAtom(schedule, timeZone),
readOnly: schedule.userId !== userId && !isManagedEventType,
isLastSchedule: schedules.length <= 1,
userId: schedule.userId,
};
});
return schedulesFormatted;
}
}
+2 -16
View File
@@ -1,22 +1,8 @@
## 1.0.108
## 1.0.110
## 1.0.114
### Patch Changes
- [#22107](https://github.com/calcom/cal.com/pull/22107) [`88a2bd3`](https://github.com/calcom/cal.com/commit/88a2bd3317a2cfaf6b367524e2736445544dbd63) Thanks [@supalarry](https://github.com/supalarry)! - test log can ignore
### Patch Changes
- [#22103](https://github.com/calcom/cal.com/pull/22103) [`61274bc`](https://github.com/calcom/cal.com/commit/61274bc7efc67b162d46b59cd75bd376ad515c51) Thanks [@supalarry](https://github.com/supalarry)! - testing changesets - can ignore this
## 1.0.107
### Patch Changes
- [#22095](https://github.com/calcom/cal.com/pull/22095) [`3c43ce4`](https://github.com/calcom/cal.com/commit/3c43ce4165bce1da29b203d5f2eb62090c5fddd2) Thanks [@supalarry](https://github.com/supalarry)! - test by updating changelog
- [#21864](https://github.com/calcom/cal.com/pull/21864) [`540bf86`](https://github.com/calcom/cal.com/commit/540bf868b5b60a98d9d3aeb565e2089f15c3dfd3) Thanks [@supalarry](https://github.com/supalarry)! - fix saving event type settings
- [#22148](https://github.com/calcom/cal.com/pull/22148) [`d27490e`](https://github.com/calcom/cal.com/commit/d27490e6f2438d353dfcf3e793c8886e723307f3) Thanks [@supalarry](https://github.com/supalarry)! - test release
## 1.0.63
+1 -1
View File
@@ -13,6 +13,6 @@ Currently supports React 18, React 19, Next 14 and Next 15.
3. Some of the versions are suffixed e.g. `1.0.102-framer` and are intended for isolated use cases, so you most probably
want to use version without any suffix e.g. `1.0.103`.
### Documentation
### Documentation
Documentation on how to get started with platform solution is [here](https://cal.com/docs/platform/quickstart) and list of atoms can be viewed
[here](https://cal.com/docs/platform/atoms/cal-provider)
+2 -5
View File
@@ -4,7 +4,7 @@
"type": "module",
"description": "Customizable UI components to integrate scheduling into your product.",
"authors": "Cal.com, Inc.",
"version": "1.0.110",
"version": "1.0.114",
"scripts": {
"dev": "yarn vite build --watch & npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify --watch",
"build": "NODE_OPTIONS='--max_old_space_size=12288' rm -rf dist && yarn vite build && npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify && mkdir ./dist/packages/prisma-client && cp -rf ../../../node_modules/.prisma/client/*.d.ts ./dist/packages/prisma-client",
@@ -37,11 +37,9 @@
"dist",
"globals.min.css"
],
"main": "./dist/cal-atoms.umd.cjs",
"module": "./dist/cal-atoms.js",
"exports": {
".": {
"require": "./dist/cal-atoms.umd.cjs",
"import": "./dist/cal-atoms.js",
"types": "./dist/index.d.ts"
},
@@ -83,7 +81,6 @@
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-awesome-query-builder": "^5.1.2",
"react-dom": "^18.0.0 || ^19.0.0"
"typescript": "^5.0.0"
}
}
+21 -5
View File
@@ -31,16 +31,32 @@
"../../types/@wojtekmaj__react-daterange-picker.d.ts",
"../../types/business-days-plugin.d.ts",
"../../types/window.d.ts",
"../../lib",
"../../features",
"../types",
"../constants",
"../utils",
"../libraries",
"../../prisma",
"../../dayjs",
"../../trpc",
"../../app-store"
"../../prisma/zod-utils.ts",
"../../prisma/zod",
"../../trpc/react",
"../../trpc/server/routers/viewer/slots",
"../../trpc/server/types",
"../../features/eventtypes",
"../../features/schedules",
"../../features/bookings/Booker",
"../../features/bookings/Booker",
"../../features/instant-meeting",
"../../features/bookings/handleMarkNoShow.ts",
"../../features/bookings/types.ts",
"../../features/bookings/lib",
"../../features/ee/workflows",
"../../features/ee/payments",
"../../features/ee/round-robin",
"../../features/ee/billing",
"../../features/shell",
"../../features/components",
"../../features/form-builder",
"../../features/data-table"
],
"exclude": [
"dist",
+5 -6
View File
@@ -3,7 +3,6 @@ import path from "path";
import { resolve } from "path";
import { defineConfig, loadEnv } from "vite";
import dts from "vite-plugin-dts";
import Inspect from "vite-plugin-inspect";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), ""); // .env inside of packages/platform/atoms
@@ -20,7 +19,7 @@ export default defineConfig(({ mode }) => {
"@calcom/platform-utils",
],
},
plugins: [Inspect(), react(), dts({ insertTypesEntry: true })],
plugins: [react(), dts({ insertTypesEntry: true })],
define: {
"process.env.NEXT_PUBLIC_WEBAPP_URL": `"${webAppUrl}"`,
},
@@ -28,17 +27,16 @@ export default defineConfig(({ mode }) => {
noExternal: ["turndown"], // Example if you want to disable SSR for your library
},
build: {
commonjsOptions: {
include: [/@calcom\/lib/, /@calcom\/features/, /node_modules/],
},
lib: {
entry: [resolve(__dirname, "index.ts")],
name: "CalAtoms",
fileName: "cal-atoms",
formats: ["es"],
},
rollupOptions: {
external: ["react", "fs", "path", "os", "react-dom", "react-awesome-query-builder"],
external: ["react", "fs", "path", "os", "react/jsx-runtime", "react-dom", "react-dom/client"],
output: {
format: "esm", // Force ESM output
globals: {
react: "React",
"react-dom": "ReactDOM",
@@ -58,6 +56,7 @@ export default defineConfig(({ mode }) => {
"@radix-ui/react-dialog": path.resolve(__dirname, "./src/components/ui/dialog.tsx"),
".prisma/client": path.resolve(__dirname, "../../prisma-client"),
"@prisma/client": path.resolve(__dirname, "../../prisma-client"),
"@calcom/prisma/client": path.resolve(__dirname, "../../prisma-client"),
"@calcom/prisma": path.resolve(__dirname, "../../prisma"),
"@calcom/dayjs": path.resolve(__dirname, "../../dayjs"),
"@calcom/platform-constants": path.resolve(__dirname, "../constants/index.ts"),
Binary file not shown.
@@ -0,0 +1,148 @@
import type { User, Team, EventType } from "@prisma/client";
import { describe, it, expect, afterAll, beforeAll } from "vitest";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { listWithTeamHandler } from "./listWithTeam.handler";
let user1: User;
let user2: User;
let team1: Team;
let team2: Team;
let eventType1: EventType;
let eventType2: EventType;
let eventType3: EventType;
let eventType4: EventType;
describe("listWithTeamHandler", () => {
beforeAll(async () => {
// Create users, teams and event types
user1 = await prisma.user.create({
data: {
username: "testuser-lwt-1",
email: "testuser-lwt-1@example.com",
name: "Test User 1",
},
});
user2 = await prisma.user.create({
data: {
username: "testuser-lwt-2",
email: "testuser-lwt-2@example.com",
name: "Test User 2",
},
});
team1 = await prisma.team.create({
data: {
name: "Team 1 lwt",
slug: "team-1-lwt",
members: {
create: {
userId: user1.id,
role: MembershipRole.MEMBER,
accepted: true,
},
},
},
});
team2 = await prisma.team.create({
data: {
name: "Team 2 lwt",
slug: "team-2-lwt",
},
});
eventType1 = await prisma.eventType.create({
data: {
title: "User 1 Event",
slug: "user1-event-lwt",
length: 30,
userId: user1.id,
},
});
eventType2 = await prisma.eventType.create({
data: {
title: "Team 1 Event",
slug: "team1-event-lwt",
length: 30,
teamId: team1.id,
userId: user1.id,
},
});
eventType3 = await prisma.eventType.create({
data: {
title: "User 2 Event",
slug: "user2-event-lwt",
length: 30,
userId: user2.id,
},
});
eventType4 = await prisma.eventType.create({
data: {
title: "Team 2 Event",
slug: "team2-event-lwt",
length: 30,
teamId: team2.id,
userId: user2.id,
},
});
});
afterAll(async () => {
await prisma.eventType.deleteMany({
where: {
id: {
in: [eventType1.id, eventType2.id, eventType3.id, eventType4.id],
},
},
});
await prisma.team.deleteMany({
where: {
id: {
in: [team1.id, team2.id],
},
},
});
await prisma.user.deleteMany({
where: {
id: {
in: [user1.id, user2.id],
},
},
});
});
it("should return user's own event types and event types of teams they are a member of", async () => {
const result = await listWithTeamHandler({
ctx: {
// we only need the id from the user object
user: { id: user1.id } as any,
},
});
expect(result).toHaveLength(2);
const resultIds = result.map((e) => e.id);
expect(resultIds).toContain(eventType1.id);
expect(resultIds).toContain(eventType2.id);
const eventType1Result = result.find((e) => e.id === eventType1.id);
expect(eventType1Result).toBeDefined();
expect(eventType1Result?.title).toBe(eventType1.title);
expect(eventType1Result?.slug).toBe(eventType1.slug);
expect(eventType1Result?.team).toBeNull();
const eventType2Result = result.find((e) => e.id === eventType2.id);
expect(eventType2Result).toBeDefined();
expect(eventType2Result?.title).toBe(eventType2.title);
expect(eventType2Result?.slug).toBe(eventType2.slug);
expect(eventType2Result?.team).toBeDefined();
expect(eventType2Result?.team?.id).toBe(team1.id);
expect(eventType2Result?.team?.name).toBe(team1.name);
});
});
@@ -1,4 +1,6 @@
import { prisma } from "@calcom/prisma";
import { Prisma } from "@prisma/client";
import db from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../types";
@@ -9,31 +11,26 @@ type ListWithTeamOptions = {
};
export const listWithTeamHandler = async ({ ctx }: ListWithTeamOptions) => {
return await prisma.eventType.findMany({
where: {
OR: [
{ userId: ctx.user.id },
{
team: {
members: {
some: {
userId: ctx.user.id,
},
},
},
},
],
},
select: {
id: true,
team: {
select: {
id: true,
name: true,
},
},
title: true,
slug: true,
},
});
const userId = ctx.user.id;
const query = Prisma.sql`SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "j1"."name" as "teamName"
FROM "public"."EventType"
LEFT JOIN "public"."Team" AS "j1" ON ("j1"."id") = ("public"."EventType"."teamId")
WHERE "public"."EventType"."userId" = ${userId}
UNION
SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "j1"."name" as "teamName"
FROM "public"."EventType"
INNER JOIN "public"."Team" AS "j1" ON ("j1"."id") = ("public"."EventType"."teamId")
INNER JOIN "public"."Membership" AS "t2" ON "t2"."teamId" = "j1"."id"
WHERE "t2"."userId" = ${userId} AND "t2"."accepted" = true`;
const result = await db.$queryRaw<
{ id: number; teamId: number | null; title: string; slug: string; teamName: string | null }[]
>(query);
return result.map((row) => ({
id: row.id,
team: row.teamId ? { id: row.teamId, name: row.teamName || "" } : null,
title: row.title,
slug: row.slug,
}));
};
@@ -9,6 +9,7 @@ import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billlin
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { HttpError } from "@calcom/lib/http-error";
import { isBase64Image } from "@calcom/lib/isBase64Image";
import logger from "@calcom/lib/logger";
import { uploadAvatar } from "@calcom/lib/server/avatar";
import { checkUsername } from "@calcom/lib/server/checkUsername";
@@ -152,7 +153,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
}
// if defined AND a base 64 string, upload and update the avatar URL
if (input.avatarUrl && input.avatarUrl.startsWith("data:image/png;base64,")) {
if (input.avatarUrl && isBase64Image(input.avatarUrl)) {
data.avatarUrl = await uploadAvatar({
avatar: await resizeBase64Image(input.avatarUrl),
userId: user.id,
@@ -2,6 +2,7 @@ import type { Prisma } from "@prisma/client";
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers";
import { isBase64Image } from "@calcom/lib/isBase64Image";
import { uploadLogo } from "@calcom/lib/server/avatar";
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
@@ -161,12 +162,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
metadata: mergeMetadata({ ...input.metadata }),
};
if (
input.banner &&
(input.banner.startsWith("data:image/png;base64,") ||
input.banner.startsWith("data:image/jpeg;base64,") ||
input.banner.startsWith("data:image/jpg;base64,"))
) {
if (input.banner && isBase64Image(input.banner)) {
const banner = await resizeBase64Image(input.banner, { maxSize: 1500 });
data.bannerUrl = await uploadLogo({
logo: banner,
@@ -177,12 +173,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
data.bannerUrl = null;
}
if (
input.logoUrl &&
(input.logoUrl.startsWith("data:image/png;base64,") ||
input.logoUrl.startsWith("data:image/jpeg;base64,") ||
input.logoUrl.startsWith("data:image/jpg;base64,"))
) {
if (input.logoUrl && isBase64Image(input.logoUrl)) {
data.logoUrl = await uploadLogo({
logo: await resizeBase64Image(input.logoUrl),
teamId: currentOrgId,
@@ -2,6 +2,7 @@ import type { Prisma, PrismaPromise, User, Membership, Profile } from "@prisma/c
import { ensureOrganizationIsReviewed } from "@calcom/ee/organizations/lib/ensureOrganizationIsReviewed";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { isBase64Image } from "@calcom/lib/isBase64Image";
import { uploadAvatar } from "@calcom/lib/server/avatar";
import { checkRegularUsername } from "@calcom/lib/server/checkRegularUsername";
import { isOrganisationAdmin, isOrganisationOwner } from "@calcom/lib/server/queries/organisations";
@@ -119,7 +120,7 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
timeZone: input.timeZone,
};
if (input.avatar && input.avatar.startsWith("data:image/png;base64,")) {
if (input.avatar && isBase64Image(input.avatar)) {
const avatar = await resizeBase64Image(input.avatar);
data.avatarUrl = await uploadAvatar({
avatar,
@@ -1,5 +1,6 @@
import { generateTeamCheckoutSession } from "@calcom/features/ee/teams/lib/payments";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { isBase64Image } from "@calcom/lib/isBase64Image";
import { uploadLogo } from "@calcom/lib/server/avatar";
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
@@ -106,7 +107,7 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
},
});
// Upload logo, create doesn't allow logo removal
if (input.logo && input.logo.startsWith("data:image/png;base64,")) {
if (input.logo && isBase64Image(input.logo)) {
const logoUrl = await uploadLogo({
logo: await resizeBase64Image(input.logo),
teamId: createdTeam.id,
@@ -4,6 +4,7 @@ import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder";
import { isBase64Image } from "@calcom/lib/isBase64Image";
import { uploadLogo } from "@calcom/lib/server/avatar";
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
import { prisma } from "@calcom/prisma";
@@ -70,7 +71,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
rrTimestampBasis: input.rrTimestampBasis,
};
if (input.logo && input.logo.startsWith("data:image/png;base64,")) {
if (input.logo && isBase64Image(input.logo)) {
data.logoUrl = await uploadLogo({ teamId: input.id, logo: input.logo });
} else if (typeof input.logo !== "undefined" && !input.logo) {
data.logoUrl = null;
+3
View File
@@ -68,6 +68,9 @@
"cache": false,
"dependsOn": []
},
"@calcom/atoms#build": {
"outputs": ["dist/**", "globals.min.css"]
},
"@calcom/api-v2#build": {
"dependsOn": ["^build"],
"env": [
+6 -18
View File
@@ -2519,8 +2519,7 @@ __metadata:
"@axiomhq/winston": ^1.2.0
"@calcom/platform-constants": "*"
"@calcom/platform-enums": "*"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.231"
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.236"
"@calcom/platform-types": "*"
"@calcom/platform-utils": "*"
"@calcom/prisma": "*"
@@ -2712,8 +2711,7 @@ __metadata:
vite-plugin-node-polyfills: ^0.22.0
peerDependencies:
react: ^18.0.0 || ^19.0.0
react-awesome-query-builder: ^5.1.2
react-dom: ^18.0.0 || ^19.0.0
typescript: ^5.0.0
languageName: unknown
linkType: soft
@@ -3567,23 +3565,13 @@ __metadata:
languageName: unknown
linkType: soft
"@calcom/platform-libraries-0.0.2@npm:@calcom/platform-libraries@0.0.2":
version: 0.0.2
resolution: "@calcom/platform-libraries@npm:0.0.2"
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.236":
version: 0.0.236
resolution: "@calcom/platform-libraries@npm:0.0.236"
dependencies:
"@calcom/features": "*"
"@calcom/lib": "*"
checksum: 61b6be1b9d0be8a54ca8b1dff4b1e4db122b3c30d9203467f5347232eaf600ca3892da45a2de8abfe75327086406c33c1f6f75cd12a7147744437bc85a0ee755
languageName: node
linkType: hard
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.231":
version: 0.0.231
resolution: "@calcom/platform-libraries@npm:0.0.231"
dependencies:
"@calcom/features": "*"
"@calcom/lib": "*"
checksum: 0abd36b041cc2ed2068c56d36aa94286f381f425b88a0d0d7cdb14498263bbece4928162d8cea00bbf4fa01f4fb60a018933f3f8e4573331d7be2b22aab2eeaa
checksum: 7670da54aa73dfe39838ad370fd5c48ab7c8fd99f812cd68bcde76fe79d2079f8885cee24b04e3f232ef18e55c2ef9ebb940d5022751baf435d3be9c517710d9
languageName: node
linkType: hard