Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90f55670dc | ||
|
|
9c53f392ca | ||
|
|
cc19a81875 | ||
|
|
67032ddaf1 | ||
|
|
2fe71c3695 | ||
|
|
21422fe027 | ||
|
|
95575291ff | ||
|
|
3e61a0633d | ||
|
|
85c9a790f2 | ||
|
|
e17d55457f | ||
|
|
d8d8bc4d00 | ||
|
|
55cc1aea59 | ||
|
|
ae383164bc | ||
|
|
0d8863fe8e | ||
|
|
51f6654635 | ||
|
|
fa11e10bcb | ||
|
|
a8f5bd6fe9 | ||
|
|
a1c725bb35 | ||
|
|
e4eeb11a61 | ||
|
|
3b0bb25251 | ||
|
|
d27490e6f2 | ||
|
|
b3c191b553 | ||
|
|
0b977e615d | ||
|
|
979e1fd28b | ||
|
|
3437c4331d |
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
Vendored
+8
@@ -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"
|
||||||
@@ -38,8 +38,7 @@
|
|||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.231",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.236",
|
||||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
|
|||||||
+24
-5
@@ -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 { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.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 { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
@@ -33,13 +34,19 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
Delete,
|
Delete,
|
||||||
Query,
|
Query,
|
||||||
|
Headers,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import {
|
||||||
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
|
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";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
@@ -51,7 +58,8 @@ import { PrismaClient } from "@calcom/prisma";
|
|||||||
export class EventTypesController_2024_04_15 {
|
export class EventTypesController_2024_04_15 {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly eventTypesService: EventTypesService_2024_04_15,
|
private readonly eventTypesService: EventTypesService_2024_04_15,
|
||||||
private readonly prismaReadService: PrismaReadService
|
private readonly prismaReadService: PrismaReadService,
|
||||||
|
private readonly organizationsRepository: OrganizationsRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -109,19 +117,30 @@ export class EventTypesController_2024_04_15 {
|
|||||||
async getPublicEventType(
|
async getPublicEventType(
|
||||||
@Param("username") username: string,
|
@Param("username") username: string,
|
||||||
@Param("eventSlug") eventSlug: 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> {
|
): Promise<GetEventTypePublicOutput> {
|
||||||
try {
|
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(
|
const event = await getPublicEvent(
|
||||||
username.toLowerCase(),
|
username.toLowerCase(),
|
||||||
eventSlug,
|
eventSlug,
|
||||||
queryParams.isTeamEvent,
|
queryParams.isTeamEvent,
|
||||||
queryParams.org || null,
|
orgSlug ?? null,
|
||||||
this.prismaReadService.prisma as unknown as PrismaClient,
|
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
|
// 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`
|
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: event,
|
data: event,
|
||||||
status: SUCCESS_STATUS,
|
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 { 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 { 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 { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
|
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
@@ -9,7 +10,14 @@ import { UsersModule } from "@/modules/users/users.module";
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule],
|
imports: [
|
||||||
|
PrismaModule,
|
||||||
|
MembershipsModule,
|
||||||
|
TokensModule,
|
||||||
|
UsersModule,
|
||||||
|
SelectedCalendarsModule,
|
||||||
|
OrganizationsModule,
|
||||||
|
],
|
||||||
providers: [EventTypesRepository_2024_04_15, EventTypesService_2024_04_15],
|
providers: [EventTypesRepository_2024_04_15, EventTypesService_2024_04_15],
|
||||||
controllers: [EventTypesController_2024_04_15],
|
controllers: [EventTypesController_2024_04_15],
|
||||||
exports: [EventTypesService_2024_04_15, EventTypesRepository_2024_04_15],
|
exports: [EventTypesService_2024_04_15, EventTypesRepository_2024_04_15],
|
||||||
|
|||||||
+9
-1
@@ -1721,7 +1721,7 @@ describe("Event types Endpoints", () => {
|
|||||||
description: "undefined booking fields",
|
description: "undefined booking fields",
|
||||||
length: 40,
|
length: 40,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
slug: "undefined-booking-fields",
|
slug: `undefined-booking-fields-${randomString()}`,
|
||||||
locations: [],
|
locations: [],
|
||||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||||
};
|
};
|
||||||
@@ -1751,6 +1751,14 @@ describe("Event types Endpoints", () => {
|
|||||||
disableOnPrefill: false,
|
disableOnPrefill: false,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
disableOnPrefill: false,
|
||||||
|
hidden: true,
|
||||||
|
isDefault: true,
|
||||||
|
required: false,
|
||||||
|
slug: "attendeePhoneNumber",
|
||||||
|
type: "phone",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
type: "radioInput",
|
type: "radioInput",
|
||||||
|
|||||||
+9
-11
@@ -46,11 +46,10 @@ export class SchedulesController_2024_04_15 {
|
|||||||
@Body() bodySchedule: CreateScheduleInput_2024_04_15
|
@Body() bodySchedule: CreateScheduleInput_2024_04_15
|
||||||
): Promise<CreateScheduleOutput_2024_04_15> {
|
): Promise<CreateScheduleOutput_2024_04_15> {
|
||||||
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
|
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
|
||||||
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: scheduleFormatted,
|
data: schedule,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,13 +59,10 @@ export class SchedulesController_2024_04_15 {
|
|||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
): Promise<GetDefaultScheduleOutput_2024_04_15 | null> {
|
): Promise<GetDefaultScheduleOutput_2024_04_15 | null> {
|
||||||
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
||||||
const scheduleFormatted = schedule
|
|
||||||
? await this.schedulesService.formatScheduleForAtom(user, schedule)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: scheduleFormatted,
|
data: schedule,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,23 +73,25 @@ export class SchedulesController_2024_04_15 {
|
|||||||
@Param("scheduleId") scheduleId: number
|
@Param("scheduleId") scheduleId: number
|
||||||
): Promise<GetScheduleOutput_2024_04_15> {
|
): Promise<GetScheduleOutput_2024_04_15> {
|
||||||
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
|
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
|
||||||
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: scheduleFormatted,
|
data: schedule,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_04_15> {
|
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_04_15> {
|
||||||
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
const schedules = await this.schedulesService.getUserSchedules(
|
||||||
const schedulesFormatted = await this.schedulesService.formatSchedulesForAtom(user, schedules);
|
user.id,
|
||||||
|
user.timeZone,
|
||||||
|
user.defaultScheduleId
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
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 { 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 { 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 { 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 { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { Schedule } from "@prisma/client";
|
import { Schedule } from "@prisma/client";
|
||||||
import { User } from "@prisma/client";
|
|
||||||
|
|
||||||
import type { ScheduleWithAvailabilities } from "@calcom/platform-libraries-0.0.2";
|
import { updateSchedule } from "@calcom/platform-libraries/schedules";
|
||||||
import { updateScheduleHandler } from "@calcom/platform-libraries-0.0.2";
|
import { ScheduleRepository } from "@calcom/platform-libraries/schedules";
|
||||||
import {
|
|
||||||
transformWorkingHoursForClient,
|
|
||||||
transformAvailabilityForClient,
|
|
||||||
transformDateOverridesForClient,
|
|
||||||
} from "@calcom/platform-libraries-0.0.2";
|
|
||||||
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchedulesService_2024_04_15 {
|
export class SchedulesService_2024_04_15 {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly schedulesRepository: SchedulesRepository_2024_04_15,
|
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) {
|
async createUserDefaultSchedule(userId: number, timeZone: string) {
|
||||||
@@ -48,7 +44,9 @@ export class SchedulesService_2024_04_15 {
|
|||||||
await this.usersRepository.setDefaultSchedule(userId, createdSchedule.id);
|
await this.usersRepository.setDefaultSchedule(userId, createdSchedule.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdSchedule;
|
const formattedSchedule = await this.getUserSchedule(userId, createdSchedule.id);
|
||||||
|
|
||||||
|
return formattedSchedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserScheduleDefault(userId: number) {
|
async getUserScheduleDefault(userId: number) {
|
||||||
@@ -56,11 +54,29 @@ export class SchedulesService_2024_04_15 {
|
|||||||
|
|
||||||
if (!user?.defaultScheduleId) return null;
|
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) {
|
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) {
|
if (!existingSchedule) {
|
||||||
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
|
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
|
||||||
@@ -71,8 +87,8 @@ export class SchedulesService_2024_04_15 {
|
|||||||
return existingSchedule;
|
return existingSchedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserSchedules(userId: number) {
|
async getUserSchedules(userId: number, timeZone: string, defaultScheduleId: number | null) {
|
||||||
return this.schedulesRepository.getSchedulesByUserId(userId);
|
return ScheduleRepository.findManyDetailedScheduleByUserId({ userId, timeZone, defaultScheduleId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUserSchedule(
|
async updateUserSchedule(
|
||||||
@@ -89,7 +105,6 @@ export class SchedulesService_2024_04_15 {
|
|||||||
this.checkUserOwnsSchedule(user.id, existingSchedule);
|
this.checkUserOwnsSchedule(user.id, existingSchedule);
|
||||||
|
|
||||||
const schedule = await this.getUserSchedule(user.id, Number(scheduleId));
|
const schedule = await this.getUserSchedule(user.id, Number(scheduleId));
|
||||||
const scheduleFormatted = await this.formatScheduleForAtom(user, schedule);
|
|
||||||
|
|
||||||
if (!bodySchedule.schedule) {
|
if (!bodySchedule.schedule) {
|
||||||
// note(Lauris): When updating an availability in cal web app, lets say only its name, also
|
// 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
|
// 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 that would be passed by the web app. If we don't, then updating schedule name will erase
|
||||||
// schedule.
|
// schedule.
|
||||||
bodySchedule.schedule = scheduleFormatted.availability;
|
bodySchedule.schedule = schedule.availability;
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateScheduleHandler({
|
return updateSchedule({
|
||||||
input: { scheduleId: Number(scheduleId), ...bodySchedule },
|
input: {
|
||||||
ctx: { user },
|
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);
|
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">) {
|
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">) {
|
||||||
if (userId !== schedule.userId) {
|
if (userId !== schedule.userId) {
|
||||||
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
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 { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
@@ -39,6 +40,7 @@ export class CalendarServiceExceptionFilter implements ExceptionFilter {
|
|||||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||||
response.setHeader("X-Request-Id", requestId.toString());
|
response.setHeader("X-Request-Id", requestId.toString());
|
||||||
|
|
||||||
|
const userContext = extractUserContext(request);
|
||||||
this.logger.error(`Calendar Service Exception Filter: ${exception?.message}`, {
|
this.logger.error(`Calendar Service Exception Filter: ${exception?.message}`, {
|
||||||
exception,
|
exception,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
@@ -46,6 +48,7 @@ export class CalendarServiceExceptionFilter implements ExceptionFilter {
|
|||||||
url: request.url,
|
url: request.url,
|
||||||
method: request.method,
|
method: request.method,
|
||||||
requestId,
|
requestId,
|
||||||
|
...userContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
response.status(400).json({
|
response.status(400).json({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { extractUserContext } from "@/lib/extract-user-context";
|
||||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
|
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
@@ -16,6 +17,7 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
|
|||||||
const statusCode = exception.getStatus();
|
const statusCode = exception.getStatus();
|
||||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||||
response.setHeader("X-Request-Id", requestId.toString());
|
response.setHeader("X-Request-Id", requestId.toString());
|
||||||
|
const userContext = extractUserContext(request);
|
||||||
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
|
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
|
||||||
exception,
|
exception,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
@@ -23,6 +25,7 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
|
|||||||
url: request.url,
|
url: request.url,
|
||||||
method: request.method,
|
method: request.method,
|
||||||
requestId,
|
requestId,
|
||||||
|
...userContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
response.status(statusCode).json({
|
response.status(statusCode).json({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { extractUserContext } from "@/lib/extract-user-context";
|
||||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||||
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
||||||
import { Catch, HttpStatus, Logger } 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";
|
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||||
response.setHeader("X-Request-Id", requestId.toString());
|
response.setHeader("X-Request-Id", requestId.toString());
|
||||||
|
|
||||||
|
const userContext = extractUserContext(request);
|
||||||
this.logger.error(`PrismaError: ${error.message}`, {
|
this.logger.error(`PrismaError: ${error.message}`, {
|
||||||
error,
|
error,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
@@ -50,6 +52,7 @@ export class PrismaExceptionFilter implements ExceptionFilter {
|
|||||||
url: request.url,
|
url: request.url,
|
||||||
method: request.method,
|
method: request.method,
|
||||||
requestId,
|
requestId,
|
||||||
|
...userContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
let message = "There was an error, please try again later.";
|
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 { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
@@ -120,6 +121,7 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
|||||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||||
response.setHeader("X-Request-Id", requestId.toString());
|
response.setHeader("X-Request-Id", requestId.toString());
|
||||||
|
|
||||||
|
const userContext = extractUserContext(request);
|
||||||
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
|
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
|
||||||
exception,
|
exception,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
@@ -127,6 +129,7 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
|||||||
url: request.url,
|
url: request.url,
|
||||||
method: request.method,
|
method: request.method,
|
||||||
requestId,
|
requestId,
|
||||||
|
...userContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
response.status(statusCode).json({
|
response.status(statusCode).json({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { extractUserContext } from "@/lib/extract-user-context";
|
||||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||||
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
||||||
import { Catch, HttpStatus, Logger } 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";
|
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||||
response.setHeader("X-Request-Id", requestId.toString());
|
response.setHeader("X-Request-Id", requestId.toString());
|
||||||
|
|
||||||
|
const userContext = extractUserContext(request);
|
||||||
this.logger.error(`ZodError: ${error.message}`, {
|
this.logger.error(`ZodError: ${error.message}`, {
|
||||||
error,
|
error,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
@@ -25,6 +27,7 @@ export class ZodExceptionFilter implements ExceptionFilter {
|
|||||||
url: request.url,
|
url: request.url,
|
||||||
method: request.method,
|
method: request.method,
|
||||||
requestId,
|
requestId,
|
||||||
|
...userContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
response.status(HttpStatus.BAD_REQUEST).json({
|
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;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findTeamIdFromClientId(clientId: string) {
|
async findTeamIdAndSlugFromClientId(clientId: string) {
|
||||||
return this.dbRead.prisma.team.findFirstOrThrow({
|
return this.dbRead.prisma.team.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
platformOAuthClient: {
|
platformOAuthClient: {
|
||||||
@@ -80,6 +80,7 @@ export class OrganizationsRepository {
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
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
|
// A booking that has been rescheduled to a new booking will also have a status of CANCELLED
|
||||||
const isDisabledRescheduling = booking.eventType?.disableRescheduling;
|
const isDisabledRescheduling = booking.eventType?.disableRescheduling;
|
||||||
// This comes from query param and thus is considered forced
|
// This comes from query param and thus is considered forced
|
||||||
const canRescheduleCancelledBooking =
|
const canBookThroughCancelledBookingRescheduleLink = booking.eventType?.allowReschedulingCancelledBookings;
|
||||||
isForcedRescheduleForCancelledBooking || booking.eventType?.allowReschedulingCancelledBookings;
|
|
||||||
const isNonRescheduleableBooking =
|
const isNonRescheduleableBooking =
|
||||||
booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED;
|
booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED;
|
||||||
|
|
||||||
@@ -131,8 +130,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNonRescheduleableBooking) {
|
if (isNonRescheduleableBooking && !isForcedRescheduleForCancelledBooking) {
|
||||||
const canReschedule = booking.status === BookingStatus.CANCELLED && canRescheduleCancelledBooking;
|
const canReschedule =
|
||||||
|
booking.status === BookingStatus.CANCELLED && canBookThroughCancelledBookingRescheduleLink;
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: canReschedule ? eventUrl : `/booking/${uid}`,
|
destination: canReschedule ? eventUrl : `/booking/${uid}`,
|
||||||
|
|||||||
@@ -652,8 +652,9 @@ const ProfileForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{extraField}
|
{extraField}
|
||||||
<p className="text-subtle mt-1 flex items-center gap-1 text-sm">
|
<p className="text-subtle mt-1 flex gap-1 text-sm">
|
||||||
<Icon name="info" /> {t("tip_username_plus")}
|
<Icon name="info" className="mt-0.5 flex-shrink-0" />
|
||||||
|
<span className="flex-1">{t("tip_username_plus")}</span>
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "5.4.9",
|
"version": "5.4.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default function Router({ form, message, errorMessage }: inferSSRProps<ty
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{form.name} | Cal.com Forms</title>
|
<title>{form?.name} | Cal.com Forms</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="mx-auto my-0 max-w-3xl md:my-24">
|
<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">
|
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
|
||||||
|
|||||||
@@ -1,11 +1,30 @@
|
|||||||
import { wrapGetServerSidePropsWithSentry } from "@sentry/nextjs";
|
import { wrapGetServerSidePropsWithSentry } from "@sentry/nextjs";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
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(
|
export const getServerSideProps = wrapGetServerSidePropsWithSentry(async function getServerSideProps(
|
||||||
context: GetServerSidePropsContext
|
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");
|
"/router");
|
||||||
|
|||||||
+3170
-765
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -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 { oauth2_v2 } from "@googleapis/oauth2";
|
||||||
import type { OAuth2Client } from "googleapis-common";
|
import type { OAuth2Client } from "googleapis-common";
|
||||||
|
|
||||||
|
import { isBase64Image } from "@calcom/lib/isBase64Image";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
|
import { uploadAvatar } from "@calcom/lib/server/avatar";
|
||||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||||
|
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
|
||||||
|
|
||||||
export async function updateProfilePhotoGoogle(oAuth2Client: OAuth2Client, userId: number) {
|
export async function updateProfilePhotoGoogle(oAuth2Client: OAuth2Client, userId: number) {
|
||||||
try {
|
try {
|
||||||
const oauth2 = new oauth2_v2.Oauth2({ auth: oAuth2Client });
|
const oauth2 = new oauth2_v2.Oauth2({ auth: oAuth2Client });
|
||||||
const userDetails = await oauth2.userinfo.get();
|
const userDetails = await oauth2.userinfo.get();
|
||||||
if (userDetails.data?.picture) {
|
const avatarUrl = userDetails.data?.picture;
|
||||||
await UserRepository.updateAvatar({ id: userId, 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) {
|
} catch (error) {
|
||||||
logger.error("Error updating avatarUrl from google calendar connect", 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 GoogleCalendarService from "@calcom/app-store/googlecalendar/lib/CalendarService";
|
||||||
import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar";
|
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 { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
|
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
|
||||||
@@ -14,6 +19,7 @@ import { Prisma } from "@calcom/prisma/client";
|
|||||||
|
|
||||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||||
|
import { updateProfilePhotoGoogle } from "../../_utils/oauth/updateProfilePhotoGoogle";
|
||||||
import { getGoogleAppKeys } from "../lib/getGoogleAppKeys";
|
import { getGoogleAppKeys } from "../lib/getGoogleAppKeys";
|
||||||
|
|
||||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
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
|
// 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)) {
|
||||||
// if (grantedScopes.includes(SCOPE_USERINFO_PROFILE)) {
|
await updateProfilePhotoGoogle(oAuth2Client, req.session.user.id);
|
||||||
// await updateProfilePhotoGoogle(oAuth2Client, req.session.user.id);
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
const selectedCalendarWhereUnique = {
|
const selectedCalendarWhereUnique = {
|
||||||
userId: req.session.user.id,
|
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 userSettings = await getUserSettings();
|
||||||
const recurrence = getRecurrence(event);
|
const recurrence = getRecurrence(event);
|
||||||
// Documentation at: https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
|
// 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?)
|
//schedule_for: "string", TODO: Used when scheduling the meeting for someone else (needed?)
|
||||||
timezone: event.organizer.timeZone,
|
timezone: event.organizer.timeZone,
|
||||||
password: userSettings?.schedule_meeting?.default_password_for_scheduled_meetings ?? undefined,
|
password: userSettings?.schedule_meeting?.default_password_for_scheduled_meetings ?? undefined,
|
||||||
agenda: event.description,
|
agenda: truncateAgenda(event.description),
|
||||||
settings: {
|
settings: {
|
||||||
host_video: true,
|
host_video: true,
|
||||||
participant_video: true,
|
participant_video: true,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import "dayjs/locale/cs";
|
|||||||
import "dayjs/locale/de";
|
import "dayjs/locale/de";
|
||||||
import "dayjs/locale/es";
|
import "dayjs/locale/es";
|
||||||
import "dayjs/locale/es-mx";
|
import "dayjs/locale/es-mx";
|
||||||
|
import "dayjs/locale/fi";
|
||||||
import "dayjs/locale/fr";
|
import "dayjs/locale/fr";
|
||||||
import "dayjs/locale/he";
|
import "dayjs/locale/he";
|
||||||
import "dayjs/locale/hu";
|
import "dayjs/locale/hu";
|
||||||
|
|||||||
@@ -1502,15 +1502,6 @@ async function handler(
|
|||||||
evt.videoCallData = undefined;
|
evt.videoCallData = undefined;
|
||||||
// To prevent "The requested identifier already exists" error while updating event, we need to remove iCalUID
|
// To prevent "The requested identifier already exists" error while updating event, we need to remove iCalUID
|
||||||
evt.iCalUID = undefined;
|
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(
|
const updateManager = await eventManager.reschedule(
|
||||||
|
|||||||
+240
@@ -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) {
|
if (message?.length && message?.length > 0 && sendTo) {
|
||||||
|
const smsMessageWithoutOptOut = message;
|
||||||
|
|
||||||
if (process.env.TWILIO_OPT_OUT_ENABLED === "true") {
|
if (process.env.TWILIO_OPT_OUT_ENABLED === "true") {
|
||||||
message = await WorkflowOptOutService.addOptOutMessage(message, locale || "en");
|
message = await WorkflowOptOutService.addOptOutMessage(message, locale || "en");
|
||||||
}
|
}
|
||||||
@@ -167,6 +169,7 @@ export async function handler(req: NextRequest) {
|
|||||||
body: message,
|
body: message,
|
||||||
scheduledDate: reminder.scheduledDate,
|
scheduledDate: reminder.scheduledDate,
|
||||||
sender: senderID,
|
sender: senderID,
|
||||||
|
bodyWithoutOptOut: smsMessageWithoutOptOut,
|
||||||
bookingUid: reminder.booking.uid,
|
bookingUid: reminder.booking.uid,
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export async function sendSmsOrFallbackEmail(props: {
|
|||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
body: string;
|
body: string;
|
||||||
sender: string;
|
sender: string;
|
||||||
|
bodyWithoutOptOut?: string;
|
||||||
bookingUid?: string | null;
|
bookingUid?: string | null;
|
||||||
userId?: number | null;
|
userId?: number | null;
|
||||||
teamId?: number | null;
|
teamId?: number | null;
|
||||||
@@ -40,7 +41,7 @@ export async function sendSmsOrFallbackEmail(props: {
|
|||||||
await sendOrScheduleWorkflowEmails({
|
await sendOrScheduleWorkflowEmails({
|
||||||
to: [fallbackData.email],
|
to: [fallbackData.email],
|
||||||
subject: fallbackData.t("notification_about_your_booking"),
|
subject: fallbackData.t("notification_about_your_booking"),
|
||||||
html: twilioData.body,
|
html: twilioData.bodyWithoutOptOut || twilioData.body,
|
||||||
replyTo: fallbackData.replyTo,
|
replyTo: fallbackData.replyTo,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -60,6 +61,7 @@ export async function scheduleSmsOrFallbackEmail(props: {
|
|||||||
body: string;
|
body: string;
|
||||||
scheduledDate: Date;
|
scheduledDate: Date;
|
||||||
sender: string;
|
sender: string;
|
||||||
|
bodyWithoutOptOut?: string;
|
||||||
bookingUid?: string | null;
|
bookingUid?: string | null;
|
||||||
userId?: number | null;
|
userId?: number | null;
|
||||||
teamId?: number | null;
|
teamId?: number | null;
|
||||||
@@ -96,7 +98,7 @@ export async function scheduleSmsOrFallbackEmail(props: {
|
|||||||
await sendOrScheduleWorkflowEmails({
|
await sendOrScheduleWorkflowEmails({
|
||||||
to: [fallbackData.email],
|
to: [fallbackData.email],
|
||||||
subject: fallbackData.t("notification_about_your_booking"),
|
subject: fallbackData.t("notification_about_your_booking"),
|
||||||
html: twilioData.body,
|
html: twilioData.bodyWithoutOptOut || twilioData.body,
|
||||||
replyTo: fallbackData.replyTo,
|
replyTo: fallbackData.replyTo,
|
||||||
sendAt: twilioData.scheduledDate,
|
sendAt: twilioData.scheduledDate,
|
||||||
referenceUid: reminder.uuid || undefined,
|
referenceUid: reminder.uuid || undefined,
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (smsMessage.length > 0) {
|
if (smsMessage.length > 0) {
|
||||||
|
const smsMessageWithoutOptOut = smsMessage;
|
||||||
|
|
||||||
if (process.env.TWILIO_OPT_OUT_ENABLED === "true") {
|
if (process.env.TWILIO_OPT_OUT_ENABLED === "true") {
|
||||||
smsMessage = await WorkflowOptOutService.addOptOutMessage(
|
smsMessage = await WorkflowOptOutService.addOptOutMessage(
|
||||||
smsMessage,
|
smsMessage,
|
||||||
@@ -196,6 +198,7 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => {
|
|||||||
phoneNumber: reminderPhone,
|
phoneNumber: reminderPhone,
|
||||||
body: smsMessage,
|
body: smsMessage,
|
||||||
sender: senderID,
|
sender: senderID,
|
||||||
|
bodyWithoutOptOut: smsMessageWithoutOptOut,
|
||||||
bookingUid: evt.uid,
|
bookingUid: evt.uid,
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
|
|||||||
@@ -107,6 +107,66 @@ const getLocationInfo = ({
|
|||||||
return { locationAvailable, locationDetails };
|
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> = ({
|
const Locations: React.FC<LocationsProps> = ({
|
||||||
isChildrenManagedEventType,
|
isChildrenManagedEventType,
|
||||||
disableLocationProp,
|
disableLocationProp,
|
||||||
@@ -168,64 +228,6 @@ const Locations: React.FC<LocationsProps> = ({
|
|||||||
locationOptions: props.locationOptions,
|
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 [showEmptyLocationSelect, setShowEmptyLocationSelect] = useState(false);
|
||||||
const defaultInitialLocation = defaultValue || null;
|
const defaultInitialLocation = defaultValue || null;
|
||||||
const [selectedNewOption, setSelectedNewOption] = useState<SingleValueLocationOption | null>(
|
const [selectedNewOption, setSelectedNewOption] = useState<SingleValueLocationOption | null>(
|
||||||
@@ -459,6 +461,7 @@ const Locations: React.FC<LocationsProps> = ({
|
|||||||
eventLocationType={eventLocationType}
|
eventLocationType={eventLocationType}
|
||||||
index={index}
|
index={index}
|
||||||
customClassNames={customClassNames?.organizerContactInput?.locationInput}
|
customClassNames={customClassNames?.organizerContactInput?.locationInput}
|
||||||
|
disableLocationProp={disableLocationProp}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ErrorMessage
|
<ErrorMessage
|
||||||
|
|||||||
@@ -484,16 +484,34 @@ class EventsInsights {
|
|||||||
: booking.attendees;
|
: booking.attendees;
|
||||||
|
|
||||||
const formattedAttendees = attendeeList
|
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 [
|
return [
|
||||||
booking.uid,
|
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,
|
noShowGuest: data.noShowGuest,
|
||||||
attendee1: formattedAttendees[0] || null,
|
...attendeeFields,
|
||||||
attendee2: formattedAttendees[1] || null,
|
|
||||||
attendee3: formattedAttendees[2] || null,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
@@ -502,33 +520,37 @@ class EventsInsights {
|
|||||||
const data = csvData.map((bookingTimeStatus) => {
|
const data = csvData.map((bookingTimeStatus) => {
|
||||||
if (!bookingTimeStatus.uid) {
|
if (!bookingTimeStatus.uid) {
|
||||||
// should not be reached because we filtered above
|
// 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 {
|
return {
|
||||||
...bookingTimeStatus,
|
...bookingTimeStatus,
|
||||||
noShowGuest: false,
|
noShowGuest: false,
|
||||||
attendee1: null,
|
...nullAttendeeFields,
|
||||||
attendee2: null,
|
|
||||||
attendee3: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const attendeeData = bookingMap.get(bookingTimeStatus.uid);
|
const attendeeData = finalBookingMap.get(bookingTimeStatus.uid);
|
||||||
|
|
||||||
if (!attendeeData) {
|
if (!attendeeData) {
|
||||||
|
const nullAttendeeFields: Record<string, null> = {};
|
||||||
|
for (let i = 1; i <= maxAttendees; i++) {
|
||||||
|
nullAttendeeFields[`attendee${i}`] = null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...bookingTimeStatus,
|
...bookingTimeStatus,
|
||||||
noShowGuest: false,
|
noShowGuest: false,
|
||||||
attendee1: null,
|
...nullAttendeeFields,
|
||||||
attendee2: null,
|
|
||||||
attendee3: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...bookingTimeStatus,
|
...bookingTimeStatus,
|
||||||
noShowGuest: attendeeData.noShowGuest,
|
noShowGuest: attendeeData.noShowGuest,
|
||||||
attendee1: attendeeData.attendee1,
|
...Object.fromEntries(Object.entries(attendeeData).filter(([key]) => key.startsWith("attendee"))),
|
||||||
attendee2: attendeeData.attendee2,
|
|
||||||
attendee3: attendeeData.attendee3,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { CookieOption, CookiesOptions } from "next-auth";
|
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
|
* 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 cookiePrefix = useSecureCookies ? "__Secure-" : "";
|
||||||
|
|
||||||
const defaultOptions: CookieOption["options"] = {
|
const defaultOptions: CookieOption["options"] = {
|
||||||
domain: isENVDev
|
domain: NEXTAUTH_COOKIE_DOMAIN || undefined,
|
||||||
? 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,
|
|
||||||
// To enable cookies on widgets,
|
// To enable cookies on widgets,
|
||||||
// https://stackoverflow.com/questions/45094712/iframe-not-reading-cookies-in-chrome
|
// https://stackoverflow.com/questions/45094712/iframe-not-reading-cookies-in-chrome
|
||||||
// But we need to set it as `lax` in development
|
// But we need to set it as `lax` in development
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView
|
|||||||
}
|
}
|
||||||
|
|
||||||
dynamicEventName = dynamicEventName.replace(`{${variable}}`, fieldValue || "");
|
dynamicEventName = dynamicEventName.replace(`{${variable}}`, fieldValue || "");
|
||||||
|
} else {
|
||||||
|
dynamicEventName = dynamicEventName.replace(`{${variable}}`, "");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,")
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import "@calcom/lib/__mocks__/logger";
|
import "@calcom/lib/__mocks__/logger";
|
||||||
|
|
||||||
|
import { createHash } from "crypto";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
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 { getUrlSearchParamsToForward } from "@calcom/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward";
|
||||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { isAuthorizedToViewFormOnOrgDomain } from "@calcom/features/routing-forms/lib/isAuthorizedToViewForm";
|
import { isAuthorizedToViewFormOnOrgDomain } from "@calcom/features/routing-forms/lib/isAuthorizedToViewForm";
|
||||||
|
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||||
import { RoutingFormRepository } from "@calcom/lib/server/repository/routingForm";
|
import { RoutingFormRepository } from "@calcom/lib/server/repository/routingForm";
|
||||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||||
|
|
||||||
import { getRoutedUrl } from "./getRoutedUrl";
|
import { getRoutedUrl } from "./getRoutedUrl";
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
|
vi.mock("@calcom/lib/checkRateLimitAndThrowError");
|
||||||
vi.mock("@calcom/app-store/routing-forms/lib/handleResponse");
|
vi.mock("@calcom/app-store/routing-forms/lib/handleResponse");
|
||||||
vi.mock("@calcom/lib/server/repository/routingForm");
|
vi.mock("@calcom/lib/server/repository/routingForm");
|
||||||
vi.mock("@calcom/lib/server/repository/user");
|
vi.mock("@calcom/lib/server/repository/user");
|
||||||
@@ -112,6 +115,7 @@ describe("getRoutedUrl", () => {
|
|||||||
it("should return notFound if form is not found", async () => {
|
it("should return notFound if form is not found", async () => {
|
||||||
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(null);
|
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(null);
|
||||||
const context = mockContext({});
|
const context = mockContext({});
|
||||||
|
|
||||||
const result = await getRoutedUrl(context);
|
const result = await getRoutedUrl(context);
|
||||||
expect(result).toEqual({ notFound: true });
|
expect(result).toEqual({ notFound: true });
|
||||||
expect(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).toHaveBeenCalledWith("form-id");
|
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", () => {
|
describe("Dry Run", () => {
|
||||||
it("should call handleResponse with isPreview:true", async () => {
|
it("should call handleResponse with isPreview:true", async () => {
|
||||||
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(mockForm as never);
|
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(mockForm as never);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const getDeterministicHashForResponse = (fieldsResponses: Record<string, unknown
|
|||||||
return hash;
|
return hash;
|
||||||
};
|
};
|
||||||
|
|
||||||
function hasEmbedPath(pathWithQuery: string) {
|
export function hasEmbedPath(pathWithQuery: string) {
|
||||||
const onlyPath = pathWithQuery.split("?")[0];
|
const onlyPath = pathWithQuery.split("?")[0];
|
||||||
return onlyPath.endsWith("/embed") || onlyPath.endsWith("/embed/");
|
return onlyPath.endsWith("/embed") || onlyPath.endsWith("/embed/");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,6 +138,70 @@ export class ScheduleRepository {
|
|||||||
isDefault: !scheduleId || defaultScheduleId === schedule.id,
|
isDefault: !scheduleId || defaultScheduleId === schedule.id,
|
||||||
isLastSchedule: schedulesCount <= 1,
|
isLastSchedule: schedulesCount <= 1,
|
||||||
readOnly: schedule.userId !== userId && !isManagedEventType,
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,8 @@
|
|||||||
## 1.0.108
|
## 1.0.114
|
||||||
|
|
||||||
## 1.0.110
|
|
||||||
|
|
||||||
### Patch Changes
|
### 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
|
- [#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
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
## 1.0.63
|
## 1.0.63
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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`.
|
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
|
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)
|
[here](https://cal.com/docs/platform/atoms/cal-provider)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Customizable UI components to integrate scheduling into your product.",
|
"description": "Customizable UI components to integrate scheduling into your product.",
|
||||||
"authors": "Cal.com, Inc.",
|
"authors": "Cal.com, Inc.",
|
||||||
"version": "1.0.110",
|
"version": "1.0.114",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "yarn vite build --watch & npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify --watch",
|
"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",
|
"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",
|
"dist",
|
||||||
"globals.min.css"
|
"globals.min.css"
|
||||||
],
|
],
|
||||||
"main": "./dist/cal-atoms.umd.cjs",
|
|
||||||
"module": "./dist/cal-atoms.js",
|
"module": "./dist/cal-atoms.js",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"require": "./dist/cal-atoms.umd.cjs",
|
|
||||||
"import": "./dist/cal-atoms.js",
|
"import": "./dist/cal-atoms.js",
|
||||||
"types": "./dist/index.d.ts"
|
"types": "./dist/index.d.ts"
|
||||||
},
|
},
|
||||||
@@ -83,7 +81,6 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.0.0 || ^19.0.0",
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
"react-awesome-query-builder": "^5.1.2",
|
"typescript": "^5.0.0"
|
||||||
"react-dom": "^18.0.0 || ^19.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,16 +31,32 @@
|
|||||||
"../../types/@wojtekmaj__react-daterange-picker.d.ts",
|
"../../types/@wojtekmaj__react-daterange-picker.d.ts",
|
||||||
"../../types/business-days-plugin.d.ts",
|
"../../types/business-days-plugin.d.ts",
|
||||||
"../../types/window.d.ts",
|
"../../types/window.d.ts",
|
||||||
"../../lib",
|
|
||||||
"../../features",
|
|
||||||
"../types",
|
"../types",
|
||||||
"../constants",
|
"../constants",
|
||||||
"../utils",
|
"../utils",
|
||||||
"../libraries",
|
"../libraries",
|
||||||
"../../prisma",
|
|
||||||
"../../dayjs",
|
"../../dayjs",
|
||||||
"../../trpc",
|
"../../prisma/zod-utils.ts",
|
||||||
"../../app-store"
|
"../../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": [
|
"exclude": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import path from "path";
|
|||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
import { defineConfig, loadEnv } from "vite";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
import dts from "vite-plugin-dts";
|
import dts from "vite-plugin-dts";
|
||||||
import Inspect from "vite-plugin-inspect";
|
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd(), ""); // .env inside of packages/platform/atoms
|
const env = loadEnv(mode, process.cwd(), ""); // .env inside of packages/platform/atoms
|
||||||
@@ -20,7 +19,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
"@calcom/platform-utils",
|
"@calcom/platform-utils",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [Inspect(), react(), dts({ insertTypesEntry: true })],
|
plugins: [react(), dts({ insertTypesEntry: true })],
|
||||||
define: {
|
define: {
|
||||||
"process.env.NEXT_PUBLIC_WEBAPP_URL": `"${webAppUrl}"`,
|
"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
|
noExternal: ["turndown"], // Example if you want to disable SSR for your library
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
commonjsOptions: {
|
|
||||||
include: [/@calcom\/lib/, /@calcom\/features/, /node_modules/],
|
|
||||||
},
|
|
||||||
lib: {
|
lib: {
|
||||||
entry: [resolve(__dirname, "index.ts")],
|
entry: [resolve(__dirname, "index.ts")],
|
||||||
name: "CalAtoms",
|
name: "CalAtoms",
|
||||||
fileName: "cal-atoms",
|
fileName: "cal-atoms",
|
||||||
|
formats: ["es"],
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
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: {
|
output: {
|
||||||
|
format: "esm", // Force ESM output
|
||||||
globals: {
|
globals: {
|
||||||
react: "React",
|
react: "React",
|
||||||
"react-dom": "ReactDOM",
|
"react-dom": "ReactDOM",
|
||||||
@@ -58,6 +56,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
"@radix-ui/react-dialog": path.resolve(__dirname, "./src/components/ui/dialog.tsx"),
|
"@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"),
|
||||||
"@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/prisma": path.resolve(__dirname, "../../prisma"),
|
||||||
"@calcom/dayjs": path.resolve(__dirname, "../../dayjs"),
|
"@calcom/dayjs": path.resolve(__dirname, "../../dayjs"),
|
||||||
"@calcom/platform-constants": path.resolve(__dirname, "../constants/index.ts"),
|
"@calcom/platform-constants": path.resolve(__dirname, "../constants/index.ts"),
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+148
@@ -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";
|
import type { TrpcSessionUser } from "../../../types";
|
||||||
|
|
||||||
@@ -9,31 +11,26 @@ type ListWithTeamOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const listWithTeamHandler = async ({ ctx }: ListWithTeamOptions) => {
|
export const listWithTeamHandler = async ({ ctx }: ListWithTeamOptions) => {
|
||||||
return await prisma.eventType.findMany({
|
const userId = ctx.user.id;
|
||||||
where: {
|
const query = Prisma.sql`SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "j1"."name" as "teamName"
|
||||||
OR: [
|
FROM "public"."EventType"
|
||||||
{ userId: ctx.user.id },
|
LEFT JOIN "public"."Team" AS "j1" ON ("j1"."id") = ("public"."EventType"."teamId")
|
||||||
{
|
WHERE "public"."EventType"."userId" = ${userId}
|
||||||
team: {
|
UNION
|
||||||
members: {
|
SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "j1"."name" as "teamName"
|
||||||
some: {
|
FROM "public"."EventType"
|
||||||
userId: ctx.user.id,
|
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);
|
||||||
select: {
|
|
||||||
id: true,
|
return result.map((row) => ({
|
||||||
team: {
|
id: row.id,
|
||||||
select: {
|
team: row.teamId ? { id: row.teamId, name: row.teamName || "" } : null,
|
||||||
id: true,
|
title: row.title,
|
||||||
name: true,
|
slug: row.slug,
|
||||||
},
|
}));
|
||||||
},
|
|
||||||
title: true,
|
|
||||||
slug: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billlin
|
|||||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { isBase64Image } from "@calcom/lib/isBase64Image";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { uploadAvatar } from "@calcom/lib/server/avatar";
|
import { uploadAvatar } from "@calcom/lib/server/avatar";
|
||||||
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
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 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({
|
data.avatarUrl = await uploadAvatar({
|
||||||
avatar: await resizeBase64Image(input.avatarUrl),
|
avatar: await resizeBase64Image(input.avatarUrl),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Prisma } from "@prisma/client";
|
|||||||
|
|
||||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||||
import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers";
|
import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers";
|
||||||
|
import { isBase64Image } from "@calcom/lib/isBase64Image";
|
||||||
import { uploadLogo } from "@calcom/lib/server/avatar";
|
import { uploadLogo } from "@calcom/lib/server/avatar";
|
||||||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||||
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
|
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
|
||||||
@@ -161,12 +162,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||||||
metadata: mergeMetadata({ ...input.metadata }),
|
metadata: mergeMetadata({ ...input.metadata }),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (input.banner && isBase64Image(input.banner)) {
|
||||||
input.banner &&
|
|
||||||
(input.banner.startsWith("data:image/png;base64,") ||
|
|
||||||
input.banner.startsWith("data:image/jpeg;base64,") ||
|
|
||||||
input.banner.startsWith("data:image/jpg;base64,"))
|
|
||||||
) {
|
|
||||||
const banner = await resizeBase64Image(input.banner, { maxSize: 1500 });
|
const banner = await resizeBase64Image(input.banner, { maxSize: 1500 });
|
||||||
data.bannerUrl = await uploadLogo({
|
data.bannerUrl = await uploadLogo({
|
||||||
logo: banner,
|
logo: banner,
|
||||||
@@ -177,12 +173,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||||||
data.bannerUrl = null;
|
data.bannerUrl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (input.logoUrl && isBase64Image(input.logoUrl)) {
|
||||||
input.logoUrl &&
|
|
||||||
(input.logoUrl.startsWith("data:image/png;base64,") ||
|
|
||||||
input.logoUrl.startsWith("data:image/jpeg;base64,") ||
|
|
||||||
input.logoUrl.startsWith("data:image/jpg;base64,"))
|
|
||||||
) {
|
|
||||||
data.logoUrl = await uploadLogo({
|
data.logoUrl = await uploadLogo({
|
||||||
logo: await resizeBase64Image(input.logoUrl),
|
logo: await resizeBase64Image(input.logoUrl),
|
||||||
teamId: currentOrgId,
|
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 { ensureOrganizationIsReviewed } from "@calcom/ee/organizations/lib/ensureOrganizationIsReviewed";
|
||||||
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
||||||
|
import { isBase64Image } from "@calcom/lib/isBase64Image";
|
||||||
import { uploadAvatar } from "@calcom/lib/server/avatar";
|
import { uploadAvatar } from "@calcom/lib/server/avatar";
|
||||||
import { checkRegularUsername } from "@calcom/lib/server/checkRegularUsername";
|
import { checkRegularUsername } from "@calcom/lib/server/checkRegularUsername";
|
||||||
import { isOrganisationAdmin, isOrganisationOwner } from "@calcom/lib/server/queries/organisations";
|
import { isOrganisationAdmin, isOrganisationOwner } from "@calcom/lib/server/queries/organisations";
|
||||||
@@ -119,7 +120,7 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
|
|||||||
timeZone: input.timeZone,
|
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);
|
const avatar = await resizeBase64Image(input.avatar);
|
||||||
data.avatarUrl = await uploadAvatar({
|
data.avatarUrl = await uploadAvatar({
|
||||||
avatar,
|
avatar,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { generateTeamCheckoutSession } from "@calcom/features/ee/teams/lib/payments";
|
import { generateTeamCheckoutSession } from "@calcom/features/ee/teams/lib/payments";
|
||||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
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 { uploadLogo } from "@calcom/lib/server/avatar";
|
||||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||||
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
|
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
|
// 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({
|
const logoUrl = await uploadLogo({
|
||||||
logo: await resizeBase64Image(input.logo),
|
logo: await resizeBase64Image(input.logo),
|
||||||
teamId: createdTeam.id,
|
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 { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||||
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
|
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
|
||||||
import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder";
|
import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder";
|
||||||
|
import { isBase64Image } from "@calcom/lib/isBase64Image";
|
||||||
import { uploadLogo } from "@calcom/lib/server/avatar";
|
import { uploadLogo } from "@calcom/lib/server/avatar";
|
||||||
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
|
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
@@ -70,7 +71,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||||||
rrTimestampBasis: input.rrTimestampBasis,
|
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 });
|
data.logoUrl = await uploadLogo({ teamId: input.id, logo: input.logo });
|
||||||
} else if (typeof input.logo !== "undefined" && !input.logo) {
|
} else if (typeof input.logo !== "undefined" && !input.logo) {
|
||||||
data.logoUrl = null;
|
data.logoUrl = null;
|
||||||
|
|||||||
@@ -68,6 +68,9 @@
|
|||||||
"cache": false,
|
"cache": false,
|
||||||
"dependsOn": []
|
"dependsOn": []
|
||||||
},
|
},
|
||||||
|
"@calcom/atoms#build": {
|
||||||
|
"outputs": ["dist/**", "globals.min.css"]
|
||||||
|
},
|
||||||
"@calcom/api-v2#build": {
|
"@calcom/api-v2#build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"env": [
|
"env": [
|
||||||
|
|||||||
@@ -2519,8 +2519,7 @@ __metadata:
|
|||||||
"@axiomhq/winston": ^1.2.0
|
"@axiomhq/winston": ^1.2.0
|
||||||
"@calcom/platform-constants": "*"
|
"@calcom/platform-constants": "*"
|
||||||
"@calcom/platform-enums": "*"
|
"@calcom/platform-enums": "*"
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.231"
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.236"
|
||||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2"
|
|
||||||
"@calcom/platform-types": "*"
|
"@calcom/platform-types": "*"
|
||||||
"@calcom/platform-utils": "*"
|
"@calcom/platform-utils": "*"
|
||||||
"@calcom/prisma": "*"
|
"@calcom/prisma": "*"
|
||||||
@@ -2712,8 +2711,7 @@ __metadata:
|
|||||||
vite-plugin-node-polyfills: ^0.22.0
|
vite-plugin-node-polyfills: ^0.22.0
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0.0 || ^19.0.0
|
react: ^18.0.0 || ^19.0.0
|
||||||
react-awesome-query-builder: ^5.1.2
|
typescript: ^5.0.0
|
||||||
react-dom: ^18.0.0 || ^19.0.0
|
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@@ -3567,23 +3565,13 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@calcom/platform-libraries-0.0.2@npm:@calcom/platform-libraries@0.0.2":
|
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.236":
|
||||||
version: 0.0.2
|
version: 0.0.236
|
||||||
resolution: "@calcom/platform-libraries@npm:0.0.2"
|
resolution: "@calcom/platform-libraries@npm:0.0.236"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@calcom/features": "*"
|
"@calcom/features": "*"
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "*"
|
||||||
checksum: 61b6be1b9d0be8a54ca8b1dff4b1e4db122b3c30d9203467f5347232eaf600ca3892da45a2de8abfe75327086406c33c1f6f75cd12a7147744437bc85a0ee755
|
checksum: 7670da54aa73dfe39838ad370fd5c48ab7c8fd99f812cd68bcde76fe79d2079f8885cee24b04e3f232ef18e55c2ef9ebb940d5022751baf435d3be9c517710d9
|
||||||
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
|
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user